Введение: почему этот вопрос собрал тысячи просмотров на Stack Overflow
Каждый, кто хоть раз писал на JavaScript, сталкивался с массивами. Они повсюду: списки товаров, очереди задач, история действий пользователя. И рано или поздно встает вопрос: «Как добавить элемент в начало массива в JavaScript?» На Stack Overflow этот вопрос набрал сотни тысяч просмотров, а ответов — десятки. Почему? Потому что за кажущейся простотой скрывается целый пласт знаний: от производительности до внутреннего устройства движка.
В этой статье мы не просто покажем unshift(). Мы заглянем «под капот»: как движок V8 обрабатывает вставку в начало, почему unshift может быть медленнее push, и какие альтернативы существуют для разных сценариев. Поехали!
Базовый способ: метод unshift()
Самый очевидный и распространенный способ — использовать встроенный метод unshift(). Он добавляет один или несколько элементов в начало массива и возвращает новую длину массива.
let fruits = ['banana', 'orange'];
let newLength = fruits.unshift('apple');
console.log(fruits); // ['apple', 'banana', 'orange']
console.log(newLength); // 3
Можно добавить сразу несколько элементов:
let numbers = [3, 4, 5];
numbers.unshift(1, 2);
console.log(numbers); // [1, 2, 3, 4, 5]
На этом можно было бы и закончить. Но давайте разберемся, что происходит внутри, когда мы вызываем unshift().
Как работает unshift() под капотом: взгляд на V8
Массив в JavaScript — это не просто непрерывный блок памяти, как в C. В современных движках (V8 в Chrome/Node.js, SpiderMonkey в Firefox) массивы могут быть реализованы двумя способами:
- Fast elements (упакованные) — непрерывный буфер, как в C-стиле. Оптимальны для чтения и записи по индексу.
- Dictionary elements (разреженные) — хеш-таблица. Используются, когда массив становится «дырявым» (пропуски индексов) или слишком большим.
Когда вы вызываете unshift() на fast-массиве, движок делает следующее:
- Выделяет новую память (или сдвигает существующую) на количество добавляемых элементов.
- Сдвигает все существующие элементы вправо на
nпозиций. - Записывает новые элементы в освободившиеся ячейки в начале.
Почему unshift медленнее push?
Метод push() добавляет элемент в конец — ему не нужно ничего сдвигать. unshift() же переписывает индексы для каждого существующего элемента. Если в массиве 1000 элементов, unshift вынужден сдвинуть все 1000. push просто дописывает в конец.
В V8 есть оптимизация: если массив пустой или почти пустой, сдвиг почти бесплатен. Но для больших массивов разница в производительности может быть в десятки раз. Особенно если делать unshift в цикле.
// Плохо: O(n²) из-за постоянного сдвига
let arr = [];
for (let i = 0; i < 10000; i++) {
arr.unshift(i);
}
// Хорошо: O(n) — добавляем в конец, потом разворачиваем
let arr2 = [];
for (let i = 0; i < 10000; i++) {
arr2.push(i);
}
arr2.reverse();
Альтернатива: spread-оператор (ES6+)
Современный и элегантный способ — использовать spread-оператор ... для создания нового массива:
let oldArray = [2, 3, 4];
let newArray = [1, ...oldArray];
console.log(newArray); // [1, 2, 3, 4]
Этот подход не мутирует исходный массив, что часто удобно в функциональном программировании (React, Redux). Но помните: создается новый массив, а старые элементы копируются. Для гигантских массивов это может быть дорого по памяти.
Добавление нескольких элементов в начало
let base = ['world', '!'];
let result = ['Hello', ', ', ...base];
console.log(result); // ['Hello', ', ', 'world', '!']
Метод splice(): универсальный, но опасный
splice() позволяет добавлять элементы в любую позицию. Для вставки в начало используйте индекс 0:
let colors = ['green', 'blue'];
colors.splice(0, 0, 'red', 'yellow');
console.log(colors); // ['red', 'yellow', 'green', 'blue']
Первый аргумент — индекс вставки, второй — сколько элементов удалить (0 — ничего не удаляем). Далее — элементы для добавления.
splice тоже сдвигает элементы, как и unshift. Более того, он еще и проверяет, нужно ли что-то удалять. Поэтому он медленнее unshift для чистой вставки в начало. Используйте его, когда нужно совместить удаление и добавление.
Сравнение производительности: unshift vs spread vs splice
Давайте посмотрим на реальные цифры. Запустите этот код в консоли браузера или Node.js:
const size = 100000;
let arr1 = Array.from({length: size}, (_, i) => i);
let arr2 = [...arr1];
let arr3 = [...arr1];
console.time('unshift');
arr1.unshift(-1);
console.timeEnd('unshift'); // ~0.5ms (для 100k)
console.time('spread');
let newArr = [-1, ...arr2];
console.timeEnd('spread'); // ~2ms (создание нового массива)
console.time('splice');
arr3.splice(0, 0, -1);
console.timeEnd('splice'); // ~1.5ms
Выводы:
unshift— самый быстрый для мутации существующего массива.spread— удобен, если нужна иммутабельность, но создает копию.splice— универсален, но чуть медленнее для простой вставки.
Когда что использовать: практические рекомендации
Выбор метода зависит от контекста:
- Нужно мутировать массив (изменить существующий): используйте
unshift(). Это самый прямой путь. - Нужна иммутабельность (не менять исходный): используйте spread-оператор
[newElement, ...arr]. Идеально для Redux reducers или React state. - Массив очень большой (сотни тысяч элементов): подумайте, можно ли вообще избежать вставки в начало. Возможно, стоит хранить