Руководство по четырём новым методам Array.prototype в JavaScript
Array.prototype
были недавно утверждены в рамках ECMAScript 2023. О том, как их использовать, читайте в этом подробном руководстве.Последняя версия стандарта языка JavaScript — ECMAScript 2023, являющаяся 14-й редакцией. В этом обновлении появились новые методы прототипа Array.
В этой статье я расскажу вам о четырёх новых методах, включая их работу с разреженными массивами и массивоподобными объектами. Если вы являетесь поклонником декларативного, функционального стиля написания программ на JavaScript, то вас ожидает приятная новость.
Важно ли сохранить исходный массив без каких-либо мутаций
Общим для всех четырёх новых методов массивов является то, что они не изменяют исходный массив, а возвращают совершенно новый массив. Вы можете задаться вопросом, почему такое поведение важно?
Вообще говоря, оставление данных без изменений имеет множество преимуществ, что и демонстрируют эти четыре новых метода массивов. Эти преимущества не ограничиваются массивами, а распространяются на все объекты JavaScript.
Хотя преимуществ много, ниже приведены некоторые из наиболее значимых преимуществ иммутабельности:
- Чистые функции: В функциональном программировании чистые функции — функции, которые всегда выдают один и тот же результат при одинаковых входных данных: у них нет побочных эффектов, и их поведение предсказуемо. Работа с этой функциональной моделью мышления идеальна, когда вы не модифицируете данные, и эти четыре новых метода массивов являются отличным дополнением по этой причине.
- Предсказуемое управление состоянием: Создание новых копий состояния объекта (или массива) делает управление состоянием более предсказуемым за счёт исключения неожиданных изменений и представления данных в конкретный момент времени с помощью новых копий. Это упрощает управление состоянием в масштабе и улучшает рассуждения об управлении состоянием в целом.
- Обнаружение изменений: Такие фреймворки, как React, используют упрощённое обнаружение изменений, сравнивая две копии state или props объекта, чтобы определить любые изменения и соответствующим образом отрендерить пользовательский интерфейс. Обнаружение изменений с помощью этих методов становится проще, поскольку мы можем сравнить два объекта в любой момент времени, чтобы определить любые изменения.
Метод toReversed()
Метод toReversed()
похож на классический метод reverse()
, но с существенным отличием. toReversed()
меняет местами элементы в массиве, не изменяя исходный массив.
Рассмотрим следующий массив фруктов:
const fruits = ["🍎apple", "🍊orange", "🍌banana"]
Теперь перевернём фрукты с помощью функции .reverse()
:
// Перевернём массив
const result = fruits.reverse()
console.log(result)
// ['🍌banana', '🍊orange', '🍎apple']
console.log(fruits)
// ['🍌banana', '🍊orange', '🍎apple']
// ↗️ исходный массив мутировал
При использовании функции reverse()
исходный массив изменяется/мутирует.
Для переворачивания массива без его мутации можно использовать метод toReversed()
, как показано ниже:
// Перевернём массив
const result = fruits.toReversed()
console.log(result)
// ['🍌banana', '🍊orange', '🍎apple']
console.log(fruits)
// ["🍎apple", "🍊orange", "🍌banana"]
// ↗️ исходный массив сохранился
Вуаля!
Если вы используете последнюю версию актуального браузера, например Chrome, вы можете зайти в консоль браузера и протестировать приведённые в статье примеры кода:
Поведение при работе с разреженными массивами
Для краткости напомним, что разреженные массивы — это массивы, не содержащие последовательных элементов. Например, рассмотрим следующее:
const numbers = [1,2,3]
// Присваиваем элементу индекс 11
numbers[11] = 12
console.log(numbers)
// [1, 2, 3, пустые × 8, 12]
В приведённом примере numbers
имеет восемь пустых слотов для элементов. numbers
— разреженный массив. Теперь вернёмся к функции toReversed()
. Как она работает с разреженными массивами?
toReversed()
никогда не возвращает разреженный массив. Если в исходном массиве были пустые слоты, то они будут возвращены как undefined
.
Рассмотрим вызов функции toReversed()
для приведённого ниже массива numbers
:
const numbers = [1,2,3]
// Присваиваем элементу индекс 11
numbers[11] = 12
numbers.toReversed()
// [12, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 3, 2, 1]
Как и ожидалось, все пустые слоты возвращаются в виде undefined
значений элементов массива.
Поведение с массивоподобными объектами
Несмотря на то, что функция toReversed()
существует именно для прототипа Array
, она может быть вызвана и для массивоподобных объектов.
Массивоподобный объект обычно имеет свойство length
и, опционально, свойства с именами целочисленных индексов. Примером массивоподобных объектов являются строковые объекты.
Функция toReversed()
сначала считывает свойство length
объекта, к которому обращается, а затем перебирает целочисленные ключи объекта от конца к началу, то есть от length - 1
до 0
. Значение каждого свойства добавляется в конец нового массива, который затем возвращается.
Давайте попробуем это сделать. Рассмотрим неправильное применение функции toReversed()
к строке:
const s = "Ohans Emmanuel"
// вызываем `toReversed` непосредственно на строке
s.toReversed()
//Uncaught TypeError: s.toReversed is not a function
Несмотря на то, что объект string
является массивоподобным объектом, эта программа неверна: мы не можем вызвать её таким образом, как string.toReversed()
, поскольку toReversed
не существует в прототипе string
.
Однако мы можем использовать метод call()
, как показано ниже:
const s = "Ohans Emmanuel"
// Array.prototype.toReversed.call(arrayLike)
Array.prototype.toReversed.call(s)
//['l', 'e', 'u', 'n', 'a', 'm', 'm', 'E', ' ', 's', 'n', 'a', 'h', 'O']
Как насчёт массивоподобного объекта? Рассмотрим приведённый ниже пример:
// Имеет свойство length и целочисленное свойство index.
const arrayLike = {
length: 5,
2: "Item #2"
}
Если бы это был стандартный массив, то он был бы разреженным, т.е. имел бы длину пять и значение во втором индексе.
Рассмотрим результат вызова toReversed
на нем:
console.log(Array.prototype.toReversed.call(arrayLike))
// [undefined, undefined, 'Item #2', undefined, undefined]
Функция toReversed()
создаёт реверсивный массив без создания разреженного массива. Как и ожидалось, пустые слоты возвращаются как undefined
.
Метод toSorted()
.toSorted()
является аналогом классического метода .sort()
.
Как вы уже догадались, в отличие от .sort()
, .toSorted()
не изменяет исходный массив. Ниже рассмотрена базовая операция сортировки с помощью .sort()
:
const list = [1, 5, 6, 3, 7, 8, 3, 7]
// Сортировка по возрастанию
const result = list.sort()
console.log(result)
// [1, 3, 3, 5, 6, 7, 7, 8]
console.log(list)
// [1, 3, 3, 5, 6, 7, 7, 8]
Как показано выше, sort()
сортирует массив на месте и, соответственно, мутирует массив. Теперь рассмотрим то же самое с функцией toSorted()
:
const list = [1, 5, 6, 3, 7, 8, 3, 7]
// Сортировка по возрастанию
const result = list.toSorted()
console.log(result)
// [1, 3, 3, 5, 6, 7, 7, 8]
console.log(list)
// [1, 5, 6, 3, 7, 8, 3, 7]
Как видно из вышеприведённого, toSorted()
возвращает новый массив с отсортированными элементами.
Заметим, что toSorted()
сохраняет тот же синтаксис, что и sort()
. Например, можно указать функцию, определяющую порядок сортировки, например, list.toSorted(compareFn)
.
Рассмотрим приведённый ниже пример:
const list = [1, 5, 6, 3, 7, 8, 3, 7]
//Сортировать массив в порядке убывания
list.toSorted((a,b) => a < b ? 1 : -1)
// [8, 7, 7, 6, 5, 3, 3, 1]
Поведение при работе с разреженными массивами
Пустые слоты всегда будут возвращаться как undefined
. Фактически, они рассматриваются так же, как если бы имели значение undefined
. Однако для этих слотов не будет вызываться функция compareFn
, и они всегда будут находиться в конце возвращаемого массива.
Рассмотрим следующий пример с массивом с пустым первым слотом:
// Обратите внимание на пустой начальный слот
const fruits = [, "🍎apple", "🍊orange", "🍌banana"]
console.log(fruits.toSorted())
// ['🍊orange', '🍌banana', '🍎apple', undefined]
Такое поведение идентично тому, что было бы, если бы начальное значение было undefined
. Рассмотрим приведённый ниже пример:
const fruits = [undefined, "🍎apple", "🍊orange", "🍌banana"]
console.log(fruits.toSorted())
// ['🍊orange', '🍌banana', '🍎apple', undefined]
Также следует учитывать, что пустые слоты (или undefined
слоты) всегда будут перемещены в конец возвращаемого массива, независимо от их положения в исходном массиве.
Рассмотрим следующий пример:
// пустой слот имеет индекс 2
const fruits = ["🍎apple", "🍊orange", , "🍌banana"]
console.log(fruits.toSorted())
// возвращается последним
// ['🍊orange', '🍌banana', '🍎apple', undefined]
// значение undefined имеет индекс 2
const otherFruits = ["🍎apple", "🍊orange", undefined , "🍌banana"]
console.log(otherFruits.toSorted())
// возвращается последним
// ['🍊orange', '🍌banana', '🍎apple', undefined]
Поведение с массивоподобными объектами
При использовании функции toSorted()
с объектами она сначала считывает свойство length
объекта this
. Затем она собирает целочисленные ключи объекта от начала до конца, т.е. от 0
до length - 1
. После сортировки она возвращает соответствующие значения в новом массиве.
Рассмотрим следующий пример со строкой:
const s = "Ohans Emmanuel"
// Array.prototype.toSorted.call(arrayLike)
Array.prototype.toSorted.call(s)
(14) [' ', 'E', 'O', 'a', 'a', 'e', 'h', 'l', 'm', 'm', 'n', 'n', 's', 'u']
Рассмотрим следующий пример с массивоподобным объектом:
// Имеет свойство length и целочисленное свойство index.
const arrayLike = {
length: 5,
2: "Item #2"
10: "Out of bound Item" // Это значение будет проигнорировано, так как длина равна 5
}
console.log(Array.prototype.toSorted.call(arrayLike))
// ['Item #2', undefined, undefined, undefined, undefined]
Метод toSpliced(start, deleteCount, ...items)
.toSpliced()
является аналогом классического метода .splice()
. Как и другие рассмотренные нами новые методы, toSpliced()
, в отличие от .splice()
, не изменяет массив, к которому обращается.
Синтаксис для toSpliced
идентичен синтаксису .splice
, как показано ниже:
toSpliced(start)
toSpliced(start, deleteCount)
toSpliced(start, deleteCount, item1)
toSpliced(start, deleteCount, item1, item2, itemN)
Добавьте новый элемент массива с помощью классической функции .splice()
, как показано ниже:
const months = ["Feb", "Mar", "Apr", "May"]
// Вставляем элемент "Jan" с индексом 0 и удаляем 0 элементов
months.splice(0, 0, "Jan")
console.log(months)
// ['Jan', 'Feb', 'Mar', 'Apr', 'May']
splice()
вставляет новый элемент массива и изменяет исходный массив. Для создания нового массива без мутации исходного массива следует использовать функцию toSpliced()
.
Рассмотрим приведённый выше пример, переписанный с использованием функции toSpliced()
:
const months = ["Feb", "Mar", "Apr", "May"]
// Вставляем элемент "Jan" с индексом 0 и удаляем 0 элементов
const updatedMonths = months.toSpliced(0, 0, "Jan")
console.log(updatedMonths)
// ['Jan', 'Feb', 'Mar', 'Apr', 'May']
console.log(months)
// ['Feb', 'Mar', 'Apr', 'May']
toSpliced()
возвращает новый массив без изменения исходного массива. Обратите внимание, что синтаксис для toSpliced()
и splice()
идентичен.
Поведение при работе с разреженными массивами
toSpliced()
никогда не возвращает разреженный массив. Поэтому пустые слоты будут возвращены как undefined
.
Рассмотрим приведённый ниже пример:
const arr = ["Mon", , "Wed", "Thur", , "Sat"];
// Начинаем с индекса 1 и удаляем 2 элемента
console.log(arr.toSpliced(1, 2));
// ['Mon', 'Thur', undefined, 'Sat']
Поведение с массивоподобными объектами
При работе с массивоподобными объектами toSpliced
получает длину объекта this
, считывает нужный целочисленный ключ и записывает результат в новый массив:
const s = "Ohans Emmanuel"
// Начинаем с индекса 0, удаляем 1 элемент, вставляем остальные элементы
console.log(Array.prototype.toSpliced.call(s, 0, 1, 2, 3));
// [2, 3, 'h', 'a', 'n', 's', ' ', 'E', 'm', 'm', 'a', 'n', 'u', 'e', 'l']
Метод with(index, value)
Особенно интересен метод массива .with()
. Во-первых, рассмотрим скобочную нотацию для изменения значения конкретного индекса массива:
const favorites = ["Dogs", "Cats"]
favorites[0] = "Lions"
console.log(favorites)
//(2) ['Lions', 'Cats']
При использовании скобочной нотации исходный массив всегда мутирует. Функция .with()
достигает того же результата — вставки элемента в определённый индекс, но не изменяет массив. Вместо этого возвращается новый массив с изменённым индексом.
Перепишем исходный пример, чтобы использовать .with()
:
const favorites = ["Dogs", "Cats"]
const result = favorites.with(0, "Lions")
console.log(result)
// ['Lions', 'Cats']
console.log(favorites)
// ["Dogs", "Cats"]
Поведение при работе с разреженными массивами
with()
никогда не возвращает разреженный массив. Поэтому пустые слоты будут возвращены как undefined
:
const arr = ["Mon", , "Wed", "Thur", , "Sat"];
arr.with(0, 2)
// [2, undefined, 'Wed', 'Thur', undefined, 'Sat']
Поведение с массивоподобными объектами
Как и другие методы, функция with()
считывает свойство length
объекта this
. Затем считывается каждый положительный целочисленный индекс (меньше length
) объекта. По мере обращения к ним он сохраняет значения их свойств в возвращаемом индексе массива.
Наконец, индекс и значение в сигнатуре вызова with(index, value)
устанавливаются в возвращаемом массиве. Рассмотрим приведённый ниже пример:
const s = "Ohans Emmanuel"
// Устанавливаем значение первого элемента
console.log(Array.prototype.with.call(s, 0, "F"));
// ['F', 'h', 'a', 'n', 's', ' ', 'E', 'm', 'm', 'a', 'n', 'u', 'e', 'l']
Заключение
Стандарт ECMAScript продолжает совершенствоваться, и использование его новых возможностей — хорошая идея. Используйте toReversed
, toSorted
, toSpliced
и with
для создания более декларативных JavaScript-приложений.