Руководство по четырём новым методам 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, вы можете зайти в консоль браузера и протестировать приведённые в статье примеры кода:

toReversed() в консоли браузераПоведение при работе с разреженными массивами
Для краткости напомним, что разреженные массивы — это массивы, не содержащие последовательных элементов. Например, рассмотрим следующее:
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-приложений.