Руководство по четырём новым методам Array.prototype в JavaScript

Источник: «A guide to the 4 new Array.prototype methods in JavaScript»
Четыре новых метода Array.prototype были недавно утверждены в рамках ECMAScript 2023. О том, как их использовать, читайте в этом подробном руководстве.

Последняя версия стандарта языка JavaScript — ECMAScript 2023, являющаяся 14-й редакцией. В этом обновлении появились новые методы прототипа Array.

В этой статье я расскажу вам о четырёх новых методах, включая их работу с разреженными массивами и массивоподобными объектами. Если вы являетесь поклонником декларативного, функционального стиля написания программ на JavaScript, то вас ожидает приятная новость.

Важно ли сохранить исходный массив без каких-либо мутаций

Общим для всех четырёх новых методов массивов является то, что они не изменяют исходный массив, а возвращают совершенно новый массив. Вы можете задаться вопросом, почему такое поведение важно?

Вообще говоря, оставление данных без изменений имеет множество преимуществ, что и демонстрируют эти четыре новых метода массивов. Эти преимущества не ограничиваются массивами, а распространяются на все объекты JavaScript.

Хотя преимуществ много, ниже приведены некоторые из наиболее значимых преимуществ иммутабельности:

Метод 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 в консоли браузера
Пробуем метод 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-приложений.

Дополнительные материалы

Предыдущая Статья

Отладка ошибок шлюза/Gateway Errors 502 и 504

Следующая Статья

Знакомство с Laravel Sushi — драйвером массива для Eloquent