Перейти к основному содержимому

Массивы в JavaScript

Разработчику Архитектору

Массивы в JavaScript

В JavaScript коллекциями являются массивы.

Пример:

let collection = [1, 2, 3];

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

В контексте веб-разработки, создания интерактивных интерфейсов и обработки данных на стороне сервера (Node.js), массивы играют ключевую роль в организации информации. Понимание их внутреннего устройства, возможностей манипулирования и ограничений необходимо для написания эффективного и надежного кода.


Природа и устройство массивов

В JavaScript массив — это объект специального типа. Несмотря на то, что он ведет себя как список или вектор, технически это объект, который наследует методы от прототипа Array.prototype. Это означает, что массивы обладают свойствами обычных объектов: у них есть тип, они могут содержать произвольные свойства, но их основное назначение — хранение упорядоченных данных.

Основной признак массива - наличие квадратных скобок []. Прямое обращение к ним происходит по формуле array[index].

Особенностью массивов в JavaScript является их динамический характер. Размер массива не фиксирован при его создании. Вы можете добавить новый элемент в конец массива, удалить существующий или изменить его значение в любой момент времени без необходимости пересоздания структуры или изменения выделения памяти вручную, как это требовалось бы в языках со статической типизацией (например, C++ или Java). Движок браузера или среда выполнения автоматически управляет выделением памяти, расширяя или сжимая пространство под данные по мере необходимости.

Элементы внутри массива нумеруются начиная с нуля. Это называется нулевой базой индексации. Первый элемент имеет индекс 0, второй — индекс 1 и так далее. Если массив содержит N элементов, последний элемент будет иметь индекс N минус 1. Такой подход позволяет легко вычислять адрес любого элемента в памяти, зная смещение относительно начала массива. Отсутствие элемента в массиве приводит к появлению значения undefined при обращении к несуществующему индексу, а не к ошибке выполнения, хотя попытка чтения несуществующего элемента может привести к неожиданным результатам в логике программы.

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

С точки зрения реализации, массивы в современных движках (таких как V8 в Chrome или SpiderMonkey в Firefox) оптимизированы для работы с определенными паттернами доступа. Если массив содержит только числа, движок может использовать специализированные представления данных для ускорения операций. Однако при смешивании типов или добавлении свойств, напоминающих обычные объекты, производительность может снижаться, так как движку приходится переходить к более универсальным механизмам хранения.


Создание и инициализация массивов

Существует несколько способов создания массивов в JavaScript, каждый из которых имеет свои особенности и области применения.

const config = Object.freeze({
apiUrl: process.env.API_URL || 'https://api.example.com',
timeout: parseInt(process.env.TIMEOUT || '5000', 10),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
retryCount: maxRetries > 0 ? Math.min(maxRetries, 5) : 0
});

Первый и наиболее распространенный способ — использование литерала массива. Синтаксис этого метода заключается в записи списка значений, разделенных запятыми, внутри квадратных скобок. Этот подход является декларативным, понятным и предпочтительным для большинства случаев. Пример создания массива чисел выглядит следующим образом: [1, 2, 3]. Можно создать массив строк, объектов или смешанных типов: ["apple", "banana", {id: 1}]. Литералы позволяют создавать пустые массивы простым способом [], что часто используется для последующего наполнения данными.

Второй метод — конструктор Array. Он вызывается через оператор new: new Array(). Без аргументов этот конструктор создает пустой массив. Однако использование конструктора с одним числовым аргументом ведет к созданию массива заданной длины, заполненного элементами undefined. Например, new Array(5) создаст массив длиной 5, где все элементы пусты, а не массив [5]. Эта особенность часто становится источником ошибок, поэтому разработчики рекомендуют избегать использования конструктора с единственным числовым параметром и отдавать предпочтение литералам. Конструктор new Array() с несколькими аргументами работает аналогично литералу: new Array(1, 2, 3) создаст массив из трех элементов.

Третий метод — использование статического метода Array.from(). Этот метод преобразует итерируемые объекты (строки, Set, Map) или массивоподобные объекты (объекты с свойством length) в настоящий массив. Это особенно полезно при работе с DOM-элементами, результатами запросов или строками, которые нужно разбить на символы. Например, Array.from("hello") вернет массив ['h', 'e', 'l', 'l', 'o']. Метод также принимает необязательный второй аргумент — функцию маппинга, которая применяется к каждому элементу при создании массива.

Четвертый метод — статический метод Array.of(). Он предназначен для создания массивов из переданных аргументов, избегая неоднозначности конструктора new Array(). Если передать одно число, Array.of(5) создаст массив [5], а не массив длиной 5. Это решение проблемы с конструктором и делает код более предсказуемым.

Пятый метод — использование методов fill() и copyWithin(). Метод fill(value, start, end) заполняет часть массива указанным значением. Это удобно для инициализации массивов одинаковыми элементами. Например, Array(5).fill(0) создаст массив [0, 0, 0, 0, 0]. Метод copyWithin(target, start, end) копирует последовательность элементов внутри самого массива, что позволяет дублировать данные без создания новых структур.

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


Доступ к элементам и изменение данных

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

// Доступ по индексу (нулевая база)
const firstTask = tasks[0];
console.log("Первая задача:", firstTask.title);

// Доступ к последнему элементу через отрицательный индекс
const lastTask = tasks[tasks.length - 1];
console.log("Последняя задача:", lastTask.title);

// Изменение существующего значения
tasks[1].status = "in-progress"; // Меняем статус второй задачи
console.log("Обновленный статус 2-й задачи:", tasks[1].status);

// Добавление элементов в конец (Push)
tasks.push({ id: 4, title: "Рефакторинг кода", status: "todo" });

// Добавление элементов в начало (Unshift)
tasks.unshift({ id: 0, title: "Самая важная задача", status: "urgent" });

// Удаление последнего элемента (Pop)
const removedTask = tasks.pop();
console.log("Удаленная задача:", removedTask.title);

// Удаление первого элемента (Shift)
const shiftedTask = tasks.shift();
console.log("Сдвинутая задача:", shiftedTask.title);

// Вставка/удаление в середине (Splice)
// Удаляем 1 элемент начиная с индекса 2 и добавляем новый
tasks.splice(2, 1, { id: 99, title: "Новая задача взамен старой", status: "review" });

// Проверка наличия элемента (includes)
const hasReview = tasks.some(t => t.status === "review");
console.log("Есть ли задача со статусом review?", hasReview);

Доступ к элементу осуществляется путем указания его индекса в квадратных скобках после имени переменной массива. Синтаксис выглядит как array[index]. Если индекс находится в допустимых пределах (от 0 до длины массива минус 1), возвращается значение элемента. Если индекс равен длине массива, доступ возвращает undefined. Отрицательные индексы также поддерживаются и интерпретируются как отсчет с конца массива: -1 указывает на последний элемент, -2 — на предпоследний и так далее. Это удобная возможность для быстрого доступа к концам массива без вычисления индекса вручную.

Запись нового значения производится тем же синтаксисом, но с присваиванием: array[index] = newValue. Если индекс находится в пределах текущего размера массива, старое значение заменяется новым. Если индекс равен длине массива, это эквивалентно добавлению элемента в конец. Если индекс больше длины массива, массив автоматически расширяется, а промежуточные элементы заполняются undefined. Важно помнить, что присвоение значения несуществующему индексу не вызывает ошибку, но может привести к непредвиденному поведению логики программы, если не проверять наличие элемента.

Изменение данных в массиве можно выполнять и с помощью методов, которые модифицируют исходный массив (мутация). Метод push() добавляет один или несколько элементов в конец массива и возвращает новую длину массива. Метод pop() удаляет последний элемент и возвращает его значение. Метод unshift() добавляет элементы в начало массива, сдвигая остальные индексы, и возвращает новую длину. Метод shift() удаляет первый элемент и возвращает его значение. Эти методы изменяют структуру массива непосредственно, что важно учитывать при передаче массивов в функции, так как изменения будут видны во всех ссылках на этот массив.

Для вставки или удаления элементов в середине массива используются методы splice() и slice(). Метод splice(start, deleteCount, ...items) удаляет элементы, начиная с позиции start, удаляя deleteCount элементов, и затем вставляет новые элементы items. Он возвращает массив удаленных элементов. Метод slice(begin, end) создает новый массив, содержащий часть исходного массива от индекса begin до end (не включая end). В отличие от splice, метод slice не изменяет исходный массив, а возвращает копию. Это полезно для получения срезов данных без риска мутации оригинала.

Также существуют методы для поиска элементов. Метод indexOf(value) возвращает индекс первого вхождения элемента или -1, если элемент не найден. Метод lastIndexOf(value) ищет с конца массива. Метод includes(value) возвращает булево значение, указывающее, присутствует ли элемент в массиве. Метод find(callback) возвращает первое значение, удовлетворяющее условию функции обратного вызова, а findIndex(callback) возвращает его индекс. Метод some(callback) проверяет, выполняется ли условие хотя бы для одного элемента, а every(callback) — для всех элементов.

Прямое изменение длины массива через свойство length также возможно. Установка length в меньшее значение обрезает массив, удаляя элементы с конца. Увеличение length добавляет пустые элементы (undefined) в конец. Это мощный инструмент, но его использование должно быть обосновано, так как оно может привести к потере данных или созданию разреженных массивов.


Итерация и обработка коллекций

Обработка массивов часто требует прохода по всем элементам для выполнения определенных действий, фильтрации, трансформации или агрегации. JavaScript предоставляет набор методов высшего порядка, которые позволяют работать с массивами функционально, делая код более лаконичным и выразительным.

// А. forEach — выполнение действия для каждого элемента (без возврата нового массива)
console.log("\n--- Обход всех задач ---");
tasks.forEach((task, index) => {
console.log(`[${index}] ${task.title} (${task.status})`);
});

// Б. Map — трансформация данных (создание нового массива)
// Пример: получаем только заголовки всех задач
const titlesOnly = tasks.map(task => task.title);
console.log("\nТолько заголовки:", titlesOnly);

// В. Filter — фильтрация данных
// Пример: оставляем только задачи, которые еще не выполнены
const pendingTasks = tasks.filter(task => task.status !== "done");
console.log("\nЗадачи в работе:", pendingTasks.map(t => t.title));

// Г. Reduce — свертка массива в одно значение
// Пример: подсчет количества задач со статусом "todo"
const todoCount = tasks.reduce((count, task) => {
return task.status === "todo" ? count + 1 : count;
}, 0); // 0 — начальное значение накопителя
console.log("\nКоличество задач 'todo':", todoCount);

// Д. Find / FindIndex — поиск конкретного элемента
const urgentTask = tasks.find(task => task.status === "urgent");
const urgentIndex = tasks.findIndex(task => task.status === "urgent");

if (urgentTask) {
console.log("\nНайдена срочная задача:", urgentTask.title, "на индексе", urgentIndex);
} else {
console.log("\nСрочных задач не найдено.");
}

// Е. Sort — сортировка (важно: sort меняет исходный массив)
// Сортируем задачи по приоритету статуса (условно: urgent > in-progress > todo > done)
const priorityOrder = { "urgent": 1, "in-progress": 2, "todo": 3, "done": 4 };

tasks.sort((a, b) => priorityOrder[a.status] - priorityOrder[b.status]);
console.log("\nОтсортированный список задач:");
tasks.forEach(t => console.log(`- ${t.title} (${t.status})`));

// Примечание: Для неизменяемой сортировки (ES2023+) можно использовать toSorted()
// const sortedCopy = tasks.toSorted((a, b) => ...);

Метод forEach(callback) выполняет указанную функцию обратного вызова для каждого элемента массива. Функция принимает три аргумента: текущее значение, индекс и сам массив. Этот метод подходит для выполнения побочных эффектов, таких как вывод данных в консоль, обновление DOM или запись в базу данных. Однако forEach не возвращает нового массива и не поддерживает остановку цикла с помощью break или return (возврат из коллбека просто пропускает следующий элемент). Для условий, требующих возврата результата или раннего выхода, лучше использовать другие методы.

Метод map(callback) создает новый массив, заполненный результатами вызова функции обратного вызова для каждого элемента исходного массива. Длина нового массива совпадает с длиной исходного. Этот метод идеален для трансформации данных: например, преобразования списка строк в список чисел, извлечения конкретного поля из объекта или умножения всех элементов на коэффициент. Функция обратного вызова должна возвращать значение, которое попадет в новый массив. Если функция ничего не возвращает, новый массив будет заполнен значениями undefined.

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

Метод reduce(callback, initialValue) сводит массив к одному значению. Функция обратного вызова принимает четыре аргумента: накопитель, текущее значение, индекс и массив. На каждой итерации результат предыдущего вызова передается как накопитель для следующего. Начальное значение накопителя можно задать вторым аргументом; если оно не указано, первым элементом массива становится начальное значение, а итерация начинается со второго элемента. Этот метод используется для суммирования чисел, подсчета частоты элементов, построения объектов из массива или создания сложных агрегатов.

Метод reduceRight() работает аналогично reduce, но проходит по массиву справа налево, начиная с последнего элемента. Это полезно, когда порядок обработки влияет на результат, например, при оценке математических выражений или обработке стеков.

Метод some(callback) возвращает true, если хотя бы один элемент массива удовлетворяет условию функции обратного вызова. Метод every(callback) возвращает true, только если все элементы удовлетворяют условию. Эти методы часто используются для валидации данных или проверки состояний.

Метод find(callback) возвращает первое значение, для которого функция обратного вызова возвращает true. Если ни один элемент не подходит, возвращается undefined. Метод findIndex(callback) возвращает индекс такого элемента или -1. Эти методы полезны для поиска конкретных записей без полного прохода по всему массиву, если условие выполняется рано.

Метод flat(depth) создает новый массив с вложенными подмассивами, сплющенными до указанного уровня глубины. По умолчанию глубина равна 1. Это удобно для работы с многомерными массивами, которые могут возникать при сложной структуре данных. Метод flatMap() объединяет map и flat в одном шаге: сначала применяется функция маппинга, затем результат сплющивается на глубину 1.

Метод entries() возвращает итератор, содержащий пары [ключ, значение] для каждого элемента массива. Это позволяет использовать цикл for...of для одновременного доступа к индексу и значению. Метод keys() возвращает итератор ключей (индексов), а values() — итератор значений. Эти методы полезны для продвинутой итерации и совместимости с другими итерируемыми объектами.


Работа с сортировкой и поиском

Сортировка элементов массива — частая задача в обработке данных. Метод sort(compareFunction) сортирует элементы массива на месте и возвращает тот же массив. По умолчанию метод сортирует элементы как строки в лексикографическом порядке. Это означает, что числа будут отсортированы неправильно: 10 окажется перед 2, так как сравниваются их строковые представления '10' и '2'. Чтобы обеспечить корректную сортировку чисел, необходимо передать функцию сравнения.

Функция сравнения принимает два аргумента: a и b. Она должна возвращать отрицательное значение, если a должен идти перед b, положительное — если после, и ноль — если они равны. Для сортировки чисел по возрастанию используется функция (a, b) => a - b. Для убывания — (a, b) => b - a. Сортировка строк с учетом регистра также требует настройки: стандартная сортировка учитывает ASCII-коды, где заглавные буквы идут перед строчными. Для игнорирования регистра можно использовать localeCompare.

Метод reverse() разворачивает массив в обратном порядке, меняя местами первый и последний элементы, второй и предпоследний и так далее. Этот метод также изменяет исходный массив.

Поиск элементов в массиве был рассмотрен ранее, но стоит отметить специфику бинарного поиска. Стандартные методы indexOf и includes используют линейный поиск, который проверяет каждый элемент последовательно. Время выполнения составляет O(n). Для больших отсортированных массивов можно реализовать бинарный поиск вручную или использовать библиотеки, но встроенных методов бинарного поиска в стандартной библиотеке JavaScript нет.

Метод toSorted() (доступен в более новых версиях ECMAScript) возвращает новый отсортированный массив, не изменяя исходный. Аналогично, toReversed() возвращает новый массив в обратном порядке. Эти методы являются частью современного функционального подхода, минимизирующего побочные эффекты.


Многомерные массивы и структура данных

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

// Создание матрицы (пример структуры данных)
const grid = [
[1, 0, 1],
[0, 1, 0],
[1, 0, 1]
];

// Доступ к элементу матрицы
const cellValue = grid[1][2]; // Значение 0
console.log("\nЗначение ячейки [1][2]:", cellValue);

// Флаттенинг (сплющивание) многомерного массива
const nestedArray = [[1, 2], [3, 4], [5, 6]];
const flatArray = nestedArray.flat(); // [1, 2, 3, 4, 5, 6]
console.log("Сплющенный массив:", flatArray);

// Использование flatMap (map + flat в одном шаге)
const words = ["hello", "world"];
// Преобразуем слова в массив символов и сплющиваем результат
const allChars = words.flatMap(word => word.split(''));
console.log("Все символы:", allChars);

Одномерный массив имеет одну размерность и доступен по одному индексу. Двумерный массив — это массив массивов, где каждый элемент является массивом. Доступ к элементу осуществляется двумя индексами: matrix[row][column]. Трехмерные и более высокие измерения возможны аналогичным образом. Однако многомерные массивы в JavaScript не имеют специальной оптимизации для таких структур, и память выделяется разрозненно. Каждый вложенный массив — это отдельный объект с собственным заголовком и выделением памяти.

Использование многомерных массивов требует внимательности при копировании. Операция присваивания массива b = a создает только ссылку на тот же объект. Изменения в b повлияют на a. Для глубокого копирования многомерных массивов требуется рекурсивное копирование или использование методов вроде JSON.parse(JSON.stringify(array)), хотя этот способ имеет ограничения (не работает с функциями, циклическими ссылками, undefined и специальными типами данных). Современные методы, такие как structuredClone(), позволяют безопасно клонировать сложные структуры данных, включая многомерные массивы, сохраняя типы и структуру.

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


Производительность и оптимизация

Производительность работы с массивами зависит от типа операций и объема данных. Линейные операции, такие как push, pop, shift, unshift, имеют различную временную сложность. Добавление и удаление в конце массива (push, pop) происходит за O(1), так как не требуется перемещение других элементов. Добавление и удаление в начале массива (unshift, shift) требуют сдвига всех остальных элементов, что занимает O(n).

Итерация по массиву с помощью forEach, map, filter и других методов высокого порядка обычно выполняется быстро, так как они реализованы на уровне движка и оптимизированы. Однако создание новых массивов на каждом шаге (например, цепочка map + filter) может приводить к увеличению нагрузки на сборщик мусора, особенно при работе с большими данными. В таких случаях рекомендуется комбинировать операции или использовать reduce для однократного прохода.

Память, выделяемая для массивов, динамически управляется. При удалении элементов из середины или начала массива освободившаяся память не всегда немедленно возвращается системе, а может оставаться в виде «дыр» (holes). Это не критично для большинства приложений, но может влиять на производительность при частом изменении структуры. Использование методов, которые создают новые массивы (например, filter), также требует дополнительного выделения памяти.

Для оптимизации работы с большими массивами следует избегать вложенных циклов, если это возможно, и использовать встроенные методы. Если требуется частый поиск по элементам, целесообразно использовать структуры данных с быстрым поиском, такие как Set или Map, вместо массива. Метод includes в массиве имеет сложность O(n), тогда как Set.has — O(1).

Также важно учитывать, что массивы в JavaScript — это объекты, и добавление произвольных свойств может замедлить работу движка, так как он переходит от оптимизированных представлений (например, плотных массивов) к общим объектам. Поэтому рекомендуется хранить только числовые индексы в массивах и использовать объекты для хранения дополнительных метаданных.


Практическое применение и примеры

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

Пример использования массива для управления списком задач:

const tasks = [
{ id: 1, title: "Написать код", completed: false },
{ id: 2, title: "Протестировать", completed: true }
];

// Добавление новой задачи
tasks.push({ id: 3, title: "Документировать", completed: false });

// Поиск незавершенных задач
const pendingTasks = tasks.filter(task => !task.completed);

// Изменение статуса задачи
tasks.forEach(task => {
if (task.id === 1) {
task.completed = true;
}
});

// Удаление выполненной задачи
const updatedTasks = tasks.filter(task => task.id !== 2);

Пример работы с многомерным массивом для представления игровой карты:

const map = [
[1, 0, 1],
[0, 0, 0],
[1, 0, 1]
];

// Получение значения ячейки
const cellValue = map[1][2]; // 0

// Обход всей карты
for (let row of map) {
for (let cell of row) {
console.log(cell);
}
}

Пример использования reduce для подсчета суммы зарплат:

const salaries = [3000, 4500, 6000, 7500];
const totalSalary = salaries.reduce((sum, salary) => sum + salary, 0);
console.log(totalSalary); // 21000