Массивы в JavaScript: методы, мутации и подводные камни
Содержание
О проверке кода. Примеры не запускались — в отличие от материалов по Node.js, где приводится фактический вывод. Говорим прямо, а не ставим бейдж «проверено» без проверки.
Массив в JavaScript — не массив в классическом смысле: это объект с числовыми ключами и изменяемой длиной. Отсюда и удобство, и все странности.
Мутирует или нет — главный вопрос
Это единственное деление, которое нужно помнить наизусть. Половина багов с массивами — от того, что метод изменил исходник, а этого не ждали.
Меняют исходный массив
Возвращают что угодно, но портят оригинал
push/pop— с концаshift/unshift— с началаsplice— вырезает изнутриsort— сортирует на местеreverse,fill,copyWithin
Возвращают новый
Исходник не трогают
map,filter,reduceslice— копирует кусокconcat,flat,jointoSorted,toReversed,toSpliced,with(ES2023)- Предсказуемы: можно в цепочку
Типичная ошибка:
const prices = [30, 10, 20];
const sorted = prices.sort(); // вернул отсортированный...
prices; // ...и ИЗМЕНИЛ исходный: [10, 20, 30]
sort возвращает тот же самый массив, а не копию. Если исходный порядок был нужен — он потерян.
// ✅ безопасно
const sorted = [...prices].sort((a, b) => a - b); // старый способ
const sorted = prices.toSorted((a, b) => a - b); // ES2023
sort() без компаратора — главная ловушка языка
[10, 9, 1].sort(); // [1, 10, 9] ← не опечатка
sort без аргумента приводит элементы к строкам и сравнивает лексикографически. Строка "10" меньше строки "9", потому что символ 1 идёт раньше 9.
[10, 9, 1].sort((a, b) => a - b); // [1, 9, 10] ✅
[10, 9, 1].sort((a, b) => b - a); // [10, 9, 1] — по убыванию
Компаратор возвращает число: отрицательное — a раньше, положительное — b раньше, ноль — порядок не важен.
Для строк с кириллицей одного < мало:
['ёж', 'яблоко', 'арбуз'].sort(); // порядок по кодам символов
['ёж', 'яблоко', 'арбуз'].sort((a, b) => a.localeCompare(b)); // ✅ по алфавиту
localeCompare знает про язык: что ё идёт после е, а не в конце алфавита.
slice и splice: одна буква, разное поведение
const arr = [1, 2, 3, 4, 5];
arr.slice(1, 3); // [2, 3] — копия куска, arr цел
arr; // [1, 2, 3, 4, 5]
arr.splice(1, 3); // [2, 3, 4] — вырезал и ВЕРНУЛ вырезанное
arr; // [1, 5] — исходник изменён!
slice(start, end) |
splice(start, count, ...items) |
|
|---|---|---|
| Исходник | не трогает | меняет |
| Возвращает | копию куска | удалённые элементы |
| Второй аргумент | индекс конца | количество |
| Умеет вставлять | нет | да |
Обратите внимание на второй аргумент: у slice это индекс, у splice — количество. Ещё одна причина их путать.
at(): последний элемент по-человечески
arr[arr.length - 1]; // старый способ
arr.at(-1); // ES2022
arr.at(-2); // предпоследний
Мелочь, но arr.at(-1) читается, а arr[arr.length - 1] — конструкция, которую глаз каждый раз разбирает заново.
Почему нельзя arr[-1]? Потому что массив — это объект, и -1 станет обычным строковым ключом "-1", а не индексом с конца. Вернётся undefined.
Длина изменяемая — и это опасно
const arr = [1, 2, 3];
arr.length = 1;
arr; // [1] — элементы просто исчезли
arr.length = 3;
arr; // [1, <2 empty items>] — дыры!
length — не только чтение. Присвоение обрезает массив, и это иногда используют как «быструю очистку» (arr.length = 0).
Опаснее другое — разреженные массивы с дырами:
const sparse = [1, , 3]; // дыра посередине
sparse.length; // 3
sparse.map((x) => x * 2); // [2, <1 empty item>, 6] — map ПРОПУСТИЛ дыру
sparse.filter(() => true); // [1, 3] — filter дыры выбросил
Методы обходятся с дырами по-разному, и запомнить это невозможно. Простое правило: не создавайте дыр. Нужен массив нужной длины — Array.from({ length: 3 }, (_, i) => i).
Копирование поверхностное
const users = [{ name: 'Аня' }];
const copy = [...users];
copy[0].name = 'Борис';
users[0].name; // 'Борис' — объект-то общий!
[...arr] и slice() копируют массив, но не то, что внутри. Объекты остаются теми же.
const deep = structuredClone(users); // глубокая копия
Тот же structuredClone, что работает в Node — это метод платформы, а не библиотека. В отличие от JSON.parse(JSON.stringify(...)) он не ломает Date, Map и Set.
Что появилось после 2017
| Метод | Версия | Что делает |
|---|---|---|
flat, flatMap |
ES2019 | разворачивает вложенность |
at() |
ES2022 | доступ с конца |
toSorted, toReversed, toSpliced, with |
ES2023 | безмутационные пары к старым |
findLast, findLastIndex |
ES2023 | поиск с конца |
Object.groupBy |
ES2024 | группировка по ключу |
Появление toSorted — показательное решение комитета. Исправить sort было нельзя: это сломало бы миллионы сайтов. Поэтому добавили новый метод рядом, а старый оставили как есть. Так язык и живёт: ошибки не исправляют, к ним приделывают правильные варианты.
Что было на этой странице в 2017 году
Первая версия вышла 15 июля 2017 года — большой справочник методов массива с примерами. Он был точным и остаётся точным: push, map, filter за девять лет не изменились.
Чего в нём не было:
- Деления на мутирующие и немутирующие. Методы шли одним списком по алфавиту, и то, что
sortпортит исходник, аsliceнет, приходилось узнавать на своих багах. toSortedи компании — их просто не существовало до ES2023. Безопасный способ отсортировать был только через копию.at()— тоже не было; писалиarr[arr.length - 1].
Ловушка sort() со строками в статье упоминалась. Она пережила девять лет и переживёт ещё столько же: исправить её нельзя, можно только знать.
Смежные темы: что такое JavaScript; ECMAScript 6 — откуда взялись стрелки и деструктуризация; глобальные объекты — про structuredClone; уроки JavaScript.
Частые вопросы
Почему `[10, 9, 1].sort()` даёт `[1, 10, 9]`?
sort без аргумента сравнивает строки, а не числа: "10" < "9". Для чисел нужен компаратор: sort((a, b) => a - b). Это самая известная ловушка массивов.Какие методы меняют исходный массив?
push, pop, shift, unshift, splice, sort, reverse, fill. Остальные — map, filter, slice, concat — возвращают новый и исходный не трогают.Чем `slice` отличается от `splice`?
slice копирует кусок и не трогает исходник. splice вырезает из исходника и меняет его. Путаница в названиях — источник постоянных багов.Как скопировать массив?
[...arr] или arr.slice() — это поверхностная копия: вложенные объекты остаются общими. Для глубокой копии есть structuredClone(arr).Что такое `toSorted()` и зачем он, если есть `sort()`?
toSorted() возвращает новый отсортированный массив, не трогая исходный. Появился в ES2023 вместе с toReversed, toSpliced и with — как безмутационные пары к старым методам.