December 25, 2021

Имитируемые методы массивов: как писать безупречно чистый код JavaScript

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

Мутация массивов в JavaScript

Массивы в JavaScript - это просто объекты, что означает, что их можно изменять. Фактически, многие встроенные методы массивов изменяют сам массив. Это может означать, что золотое правило, описанное выше, будет нарушено просто при использовании одного из встроенных методов.

Вот пример, показывающий, как это потенциально может вызвать некоторые проблемы:

const numbers = [1,2,3];
const countdown = numbers.reverse();

Этот код выглядит нормально. У нас есть массив с именем numbers, и мы хотим, чтобы другой массив с именем countdown перечислял числа в обратном порядке. И, кажется, это работает. Если проверить значение переменной countdown, то это то, что мы ожидаем:

countdown
<< [3,2,1]

Неприятным сопутствующим результатом этой операции является то, что метод reverse() изменил и массив numbers. Это совсем не то, чего мы хотели:

Что еще хуже, обе переменные ссылаются на один и тот же массив, поэтому любые изменения, которые мы впоследствии внесем в одну из них, повлияют на другую. Предположим, мы используем метод Array.prototype.push(), чтобы добавить значение 0 в конец массива countdown. То же самое будет сделано и с массивом numbers (поскольку они оба ссылаются на один и тот же массив):

countdown.push(0)
<< 4
countdown
<< [3,2,1,0]
numbers
<< [3,2,1,0]

Именно этот результат может остаться незамеченным - особенно в большом приложении - и привести к очень трудно отслеживаемым ошибкам.

Методы изменяющегося массива в JavaScript

И reverse - не единственный метод массива, который вызывает подобные мутации. Вот список методов массива, которые изменяют массив, к которому они обращаются:

Немного смущает то, что у массивов также есть некоторые методы, которые не изменяют исходный массив, а возвращают новый массив:

Эти методы возвращают новый массив в зависимости от операции, которую они выполнили. Например, метод map() можно использовать для удвоения всех чисел в массиве:

const numbers = [1,2,3];
const evens = numbers.map(number => number * 2);
<< [2,4,6]

Теперь, если мы проверим массив numbers, то увидим, что вызов метода не повлиял на него:

numbers
<< [1,2,3]

Кажется, нет никакой причины, почему одни методы мутируют массив, а другие нет. Может быть трудно запомнить, какие из них что делают.

В Ruby есть хорошее решение этой проблемы в том, как он использует нотацию "bang". Любой метод, вызывающий постоянное изменение вызывающего его объекта, заканчивается символом !. [1,2,3].reverse! перевернет массив, а [1,2,3].reverse вернет новый массив с перевернутыми элементами.

Имитируемые методы массивов: Давайте исправим этот мутирующий беспорядок!

Мы выяснили, что мутации могут быть потенциально вредными и что многие методы массивов вызывают их. Давайте посмотрим, как можно избежать их использования.

Не так уж сложно написать несколько функций, которые возвращают новый объект массива вместо того, чтобы мутировать исходный массив. Эти функции и будут нашими иммутабельными методами массива.

Поскольку мы не собираемся исправлять Array.prototype, эти функции всегда будут принимать сам массив в качестве первого параметра.

Pop

Давайте начнем с написания новой функции pop, которая возвращает копию исходного массива, но без последнего элемента. Обратите внимание, что Array.prototype.pop() возвращает значение, которое было извлечено из конца массива:

const pop = array => array.slice(0,-1);

Эта функция использует Array.prototype.slice() для возврата копии массива, но с удаленным последним элементом. Второй аргумент -1 означает прекращение резки за 1 место до конца. Мы можем увидеть, как это работает, на примере ниже:

const food = ['🍏','🍌','🥕','🍩'];
pop(food)
<< ['🍏','🍌','🥕']

Push

Далее создадим функцию push(), которая будет возвращать новый массив, но с добавленным в конец новым элементом:

const push = (array, value) => [...array,value];

Здесь используется оператор spread для создания копии массива. Затем он добавляет значение, указанное в качестве второго аргумента, в конец нового массива. Вот пример:

const food = ['🍏','🍌','🥕','🍩'];
push(food,'🍆')
<< ['🍏','🍌','🥕','🍩','🍆']

Shift и Unshift

Аналогично мы можем написать замены для Array.prototype.shift() и Array.prototype.unshift():

const shift = array => array.slice(1);

Для нашей функции shift() мы просто отрезаем первый элемент из массива вместо последнего. Это можно увидеть на примере ниже:

const food = ['🍏','🍌','🥕','🍩'];
shift(food)
<< ['🍌','🥕','🍩']

Наш метод unshift() вернет новый массив с новым значением, добавленным в начало массива:

const unshift = (array,value) => [value,...array];

Оператор spread позволяет нам размещать значения внутри массива в любом порядке. Мы просто помещаем новое значение перед копией исходного массива. Как это работает, можно увидеть на примере ниже:

const food = ['🍏','🍌','🥕','🍩'];
unshift(food,'🍆')
<< ['🍆','🍏','🍌','🥕','🍩']

Reverse

Теперь давайте попробуем написать замену для метода Array.prototype.reverse(). Он будет возвращать копию массива в обратном порядке, вместо того чтобы изменять исходный массив:

const reverse = array => [...array].reverse();

Этот метод по-прежнему использует метод Array.prototype.reverse(), но применяется к копии исходного массива, которую мы создаем с помощью оператора spread. Нет ничего плохого в том, чтобы мутировать объект сразу после его создания, что мы здесь и делаем. Мы можем видеть, как это работает в примере ниже:

const food = ['🍏','🍌','🥕','🍩'];
reverse(food)
<< ['🍩','🥕','🍌','🍏']

Splice

Наконец, давайте разберемся с Array.prototype.splice(). Это очень универсальная функция, поэтому мы не будем полностью переписывать то, что она делает. Вместо этого мы сосредоточимся на двух основных вариантах использования функции slice: удаление элементов из массива и вставка элементов в массив.

Удаление элемента массива

Начнем с функции, которая вернет новый массив, но с удаленным элементом с заданным индексом:

const remove = (array, index) => [...array.slice(0, index),...array.slice(index + 1)];

Для этого используется Array.prototype.slice(), чтобы разделить массив на две половины - по обе стороны от элемента, который мы хотим удалить. Первый срез возвращает новый массив, копируя элементы исходного массива до индекса перед элементом, указанным в качестве аргумента. Второй фрагмент возвращает массив с элементами после удаляемого элемента, вплоть до конца исходного массива. Затем мы помещаем их оба вместе в новый массив с помощью оператора spread.

Мы можем проверить, что это работает, попытавшись удалить элемент с индексом 2 в приведенном ниже массиве food:

const food = ['🍏','🍌','🥕','🍩'];
remove(food,2)
<< ['🍏','🍌','🍩']

Добавление элемента массива

Наконец, давайте напишем функцию, которая будет возвращать новый массив с новым значением, вставленным в определенный индекс:

const insert = (array,index,value) => [...array.slice(0, index), value, ...array.slice(index)];

Эта функция работает аналогично функции remove(). Она создает два фрагмента массива, но на этот раз включает элемент с указанным индексом. Когда мы снова соединяем эти два фрагмента вместе, мы вставляем между ними значение, указанное в качестве аргумента.

Мы можем проверить, как это работает, попробовав вставить эмодзи в виде кекса в середину нашего массива продуктов:

const food = ['🍏','🍌','🥕','🍩']
insert(food,2,'🧁')
<< ['🍏','🍌','🧁','🥕','🍩']

Теперь у нас есть набор методов неизменяемого массива, которые оставляют наши исходные массивы в покое. Я сохранил их все в одном месте на CodePen, поэтому смело копируйте их и используйте в своих проектах. Вы можете разделить их по пространствам имен, сделав их методами одного объекта, или просто использовать их как есть, когда это необходимо.

Этих методов достаточно для большинства операций с массивами. Если вам нужно выполнить другую операцию, помните золотое правило: сначала создайте копию исходного массива с помощью оператора spread. Затем сразу же примените к этой копии все мутирующие методы.

Источник: https://www.sitepoint.com/immutable-array-methods-javascript/