Я исследую реализацию 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. Если вы еще этого не сделали, обязательно ознакомьтесь с другими статьями этой серии:
Я собираюсь объяснить и реализовать каждый встроенный метод. Если вам это интересно, я бы посоветовал вам подписаться на меня в Твиттере, чтобы не пропустить следующие статьи из этой серии.