Я исследую реализацию pop и push в пятой статье этой серии. В частности, как мы можем добавить элемент без использования push или удалить элемент без использования pop?

Методы массива, которые я рассматривал до сих пор в этой серии, были сосредоточены на циклическом обходе массива с какой-то целью: сделать что-то с каждым элементом, получить модифицированную версию каждого элемента или получить часть массива. Это включает просмотр forEach, filter и map.

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

Как и в предыдущих статьях, я расскажу, как реализовать ваши собственные версии pop и push. С учетом сказанного, в 90% случаев вы захотите использовать эти встроенные методы. Цель рассмотрения того, как реализовать свои собственные, — понять, как получить одинаковую функциональность в случаях, когда вы не можете использовать pop и push. Например, в частях создания приложения React, где состояние неизменно (не может быть изменено).

Как они работают

Вы используете push для добавления элемента в конец массива и pop для удаления элемента из конца массива:

const numbers = [1, 2, 3, 4]
// Add an item to the end:
numbers.push(5) // [1, 2, 3, 4, 5]
// Removethe last item:
numbers.pop() // [1, 2, 3, 4 ]

Здесь я беру массив (numbers) и добавляю элемент (5) в конец с push. Затем я сразу удаляю его с помощью pop.

Существуют отдельные соответствующие методы для добавления и удаления из начала массива. Это shift и unshift, и я расскажу о них в следующей статье.

Добавление без использования push

Как добавить элемент в конец массива без использования метода push?

Самый простой способ — присвоить значение позиции индекса в массиве:

const numbers = [1, 2, 3, 4]
numbers[ numbers.length ] = 5

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

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

Вы можете создать новый массив с добавленным элементом, используя оператор распространения, который я рассмотрел в своей реализации concat. С оператором распространения я могу реализовать свою собственную версию push, которая имитирует то же поведение.

Для этого мне нужно определить функцию, которая принимает массив и новый элемент в качестве аргументов. Затем ему нужно вернуть новый массив со всеми значениями исходного массива плюс новый элемент. Что-то вроде этого:

function push(array, item) {
  return [
    ...array,
    item
  ]
}

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

let numbers = [1, 2, 3, 4]
numbers = push(numbers, 5) // [1, 2, 3, 4, 5]

В этой реализации отсутствует одна вещь: встроенный метод push позволяет добавлять несколько значений одновременно:

let numbers = [1, 2, 3, 4]
numbers.push(5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]

Изменить мою реализацию, чтобы приспособиться к этому, на самом деле довольно просто с оператором распространения и остатка:

function push(array, ...items) {
  return [
    ...array,
    ...items
  ]
}

В строке 1 я беру любое количество аргументов после исходного массива и объединяю их в массив с именем items, используя оператор rest. Затем я использую оператор spread, чтобы поместить эти элементы в новый массив вместе с элементами переданного массива.

(Обратите внимание, что может немного сбивать с толку, когда ... выполняет операцию расширения, а не операцию остатка. Еще больше сбивает с толку то, что этот оператор работает и с объектами!)

Теперь моя реализация может принимать любое количество аргументов после исходного массива:

let numbers = [1, 2, 3, 4]
// Adding one new item:
numbers = push(numbers, 5) // [1, 2, 3, 4, 5]
// Adding more than one:
numbers = push(numbers, 6, 7, 8) // 1, 2, 3, 4, 5, 6, 7, 8]

Удаление без выталкивания

Удаление последнего элемента из массива без использования pop немного сложно. Лучший подход будет немного зависеть от ваших обстоятельств и, вероятно, будет включать другой метод массива. Я покажу вам один подход, который работает не так хорошо, как вы думаете, другой, который работает нормально, а затем тот, который я считаю лучшим.

Если вы сталкивались с delete в JavaScript, вы можете перейти к нему как к одному из подходов. Я бы вообще рекомендовал избегать delete. В этом случае он удалит элемент из массива, но поведение, которого вы, возможно, не ожидаете, заключается в том, что длина массива останется прежней, а в конце останется пустой элемент (или слот):

let numbers = [1, 2, 3, 4]
// The original length:
numbers.length // 4
// Delete the last item:
delete numbers[ numbers.length - 1]
// The new length:
numbers.length // 4
numbers // [1, 2, 3, <1 empty slot>]

Если бы вы позже зациклились на этом массиве, длина не соответствовала бы количеству фактических значений в массиве, и вы бы получили какое-то странное, трудно отлаживаемое поведение:

for (let i = 0; i < numbers.length; i++) {
  console.log(numbers[i])
}
// 1
// 2
// 3
// undefined

Другой подход — написать цикл for, создающий новый массив. Я могу сделать это внутри функции, чтобы имитировать поведение pop:

function pop(array) {
  let res = []
  for (let i = 0; i < array.length - 1; i++) {
    res[i] = array[i]
  }
  
  return res
}

В приведенном выше методе pop я беру исходный массив в качестве аргумента. Затем я немедленно создаю новый пустой массив, который я в конечном итоге верну обратно вызывающей стороне. Затем я перебираю исходный массив, но пропускаю последний элемент (i < array.length -1), который фактически удаляет его. Внутри тела этого цикла я присваиваю значение в текущей позиции переданного массива позиции нового массива (res[i] = array[i]).

Это работает просто отлично:

let numbers = [1, 2, 3, 4]
numbers = pop(numbers) // [1, 2, 3]

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

Я могу достичь той же цели с помощью slice и пропустить цикл. Предостережение в том, что многим людям трудно (разумно) запомнить разницу между slice и splice. Я обнаружил, что изучение Go помогло мне. Срез в Go — это структура данных, эквивалентная массиву в JavaScript, где массив в Go действует как более традиционный тип массива. Массивы в Go имеют фиксированную длину, которую нельзя изменить после инициализации. А то я не слышал хорошей мнемоники для запоминания разницы.

В любом случае, я действительно могу упростить реализацию моего метода pop, используя slice:

function pop(array) {
  return array.slice(0, array.length - 1)
}

В следующей статье этой серии будет рассмотрена реализация как slice, так и splice. А пока вам нужно знать, что передаваемые аргументы являются начальной и конечной позициями среза. Затем slice возвращает неглубокую копию этой части массива.

Используя новую версию моего метода pop, я могу получить тот же результат:

let numbers = [1, 2, 3, 4]
numbers = pop(numbers) // [1, 2, 3]
numbers = pop(numbers) // [1, 2]
numbers = pop(numbers) // [1]

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

Вывод

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

Я собираюсь объяснить и реализовать каждый встроенный метод. Если вам это интересно, я бы посоветовал вам подписаться на меня в Твиттере, чтобы не пропустить следующие статьи из этой серии.