логотип PurpleSchool

Массивы в JavaScript не то, чем кажутся

10 августа 2023 г.
674 просмотра
фото команды
Автор

Валерий

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

Что такое массивы в JavaScript

Массивы в JavaScript являются особым подвидом объектов, в котором ключи это цифры, начиная с нуля. Такие “ключи” обычно называют индексами и они присваиваются элементом массива автоматически при добавлении. Мы можем обратиться к любому элементу массива почти так же как обращаемся к свойствам объекта, но в случае с массивами, индекс элемента нужно указывать в квадратных скобках или с помощью нового метода at, который в отличии от первого способа, может принимать и отрицательные значения. Это удобно, если нужно найти последний элемент массива, для чего нужно передать в метод at значение -1. Раньше для такой операции приходилось вычислять индекс последнего элемента отнимая единицу от длинны массива: const lastItem = arr[arr.length -1].

const myArr = [1, 2, 3, 4, 5];
console.log(myArr[1]); // 2
console.log(myArr[-2]); // undefined
console.log(myArr.at(1)); // 2
console.log(myArr.at(-2)); // 4
const myObj = { ...myArr };
console.log(myObj); // { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5 }

Способы создания

Массив можно объявить двумя способами: с помощью конструктора (const myArr = new Array(<содержимое массива или количество элементов цифрой>)) или просто обернув нужные значения в квадратные скобки (const myArr = [<элементы массива через запятую>]). Первый способ практически не используется в реальной жизни, а глобальный объект Array в основном полезен благодаря своим методам isArray, который проверяет является ли переданный параметр массивом и возвращает результат проверки в виде булева значения, а так же from, который, в отличии от конструктора, не просто принимает набор значений, а создает новый массив из итерируемого или псевдомассива (например, строки или объекта). При этом метод from может принимать необязательными параметрами функцию, которая будет вызвана для каждого элемента нового массива перед добавлением и аргумент, который будет использоваться в качестве this при выполнении такой функции:

const myArr = [1, 2, 3, 4, 5];
console.log(Array.isArray(myArr)); //true
console.log(Array.from("string")); //[ 's', 't', 'r', 'i', 'n', 'g' ]
function multiply(value) {
  return value * this.multiplier;
}
console.log(Array.from(myArr, multiply, { multiplier: 3 })); // [ 3, 6, 9, 12, 15 ]

Вложенность и клонирование

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

const nestedArr = [1, 2];
const nestedObg = { name: "John" };
const arr = ["a", nestedArr, nestedObg];
console.log(arr[1][0], arr[2].name); //1 John
const clone1 = arr; //простое присвоение значения по ссылке
const clone2 = JSON.parse(JSON.stringify(arr)); //а здесь созлается полностью новый массив
nestedArr[0] = 3;
nestedObg.name = "Stan";
console.log(arr, clone1);
// Оригинал: [ 'a', [ 3, 2 ], { name: 'Stan' } ]
// Клон:     [ 'a', [ 3, 2 ], { name: 'Stan' } ]
//после изменения вложенного массива эти изменения затронут и клон
console.log(clone2); //[ 'a', [ 1, 2 ], { name: 'John' } ]
//здесь клон не зависит от оригинала

Важные особенности

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

Добавление и удаление элементов.

Производительность сильно зависит от того с какой частью массива мы работает. Если воспользоваться методами, которые изменяют первый элемент (с индексом 0), а именно unshift для добавления элемента в начало массива или shift для удаления первого элемента - индексы всех элементов массива будут перезаписаны на новые (сдвинутся на 1). Такая операция займет гораздо больше времени и ресурсов, чем добавление элемента в конец массива методом push или удаление последнего элемента с помощью метода pop. Поэтому при работе с массивами крайне желательно использовать именно push и pop.

const myArr = new Array(10000000).fill(1);
for (let i in myArr) {
  myArr[i] = myArr[i] * 2;
} //займет 1151 мс
for (let i of myArr) {
  i = i * 2;
} //займет 141 мс

Разница не выглядит критичной, но при обработке большого количества массивов или на слабом железе она может сильно повлиять на производительность.

Разряженные и плотные массивы

В примере с добавлением и удалением элементов мы создали огромный массив с помощью конструктора и заполнили его через метод fill, но что будет если не заполнять такой массив? В таком случае он будет наполнен пустыми элементами. Массивы содержащие пустые элементы называются разряженными, а те что не имеют таковых - плотными. Разряженный массив так же можно создать прямо указав длину (arr.length = <значение целым числом>), если присвоенное значение длинны больше количества элементов в массиве. Или даже случайно, просто поставив две запятые после очередного элемента при описании массива вручную. Ни одна IDE не подсветит этот участок кода поскольку такое описание не нарушает синтаксис.

const myArr = new Array(5);
console.log(myArr); //[ <5 empty items> ]
const myArr2 = [];
myArr2.length = 5;
console.log(myArr2); //[ <5 empty items> ]
const myArr3 = [, , , , ,];
console.log(myArr3); //[ <5 empty items> ]
console.log(myArr[0], myArr2[1], myArr3[2]);
//undefined undefined undefined
myArr[0] = "value";
console.log(myArr); //[ 'value', <4 empty items> ]

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

Перебор массивов

Вы же помните что массивы это особый подвид объектов в JavaScript? Это значит что мы можем итерировать их с помощью цикла for…in…, но для массивов существует специальный цикл for…of… . Разница между ними не только в синтаксисе (в for…in… переменная это ключ объекта, а в for…of… - элемент массива ), но и в скорости работы с массивами:

const myArr = new Array(10000000).fill(1);
for (let i in myArr) {
  myArr[i] = myArr[i] * 2;
} //займет 1151 мс
for (let i of myArr) {
  i = i * 2;
} //займет 141 мс

Нечисловые свойства

Мы уже знаем, что кроме обращения к элементам массива по индексу можно напрямую обратиться к его длине и даже изменять ее, а что будет, если прямо задать нашему массиву ключ и присвоить этому ключу значение?

const myArr = [1, 2, 3];
myArr.type = "numbers";
console.log(myArr.length); // длинна равна 3
for (let i of myArr) {
  console.log(i);
} // в консоли будут только числовые свойства 1 2 3
for (let i in myArr) {
  console.log(myArr[i]);
} // в консоль выведется и нечисловое свойство numbers
console.log(myArr.type); // numbers
myArr.push(4);
//элемент с нечисловым ключем не повлияет на индексы
//новых элементов массива
console.log(myArr[3]); //4

Как видно из примера, не произойдет ни какой ошибки, специфичные для массивов методы и способы перебора будут просто игнорировать присвоенную нами пару ключ-значение. У массива не изменится длинна, а for...in... не проитерирует новый элемент. Казалось бы мы получили объект со всеми преимуществами массива, но без ограничений по виду ключей! Такие структуры данных называются псевдомассивами. Но на самом деле даже одно кастомное свойство ломает всю “магию” оптимизации массивов:

const myArr = new Array(10000000).fill("some value");
myArr.someKey = "some value";
myArr.push("new value");//выполнится за 28 мс

Операция добавления нового элемента в конец псевдомассива занимает больше времени, чем добавление элемента в начало настоящего массива. Интересно что цикл for…of… перебирает псевдомассивы так же быстро, как и обычные. Псевдомассивы могут найти свое применение, но только если вы точно знаете что в данной ситуации вам нужно именно такое решение, в противном случае вы получите просадку производительности.

Заключение

Давайте резюмируем что нам нужно помнить при работе с массивами JavaScript:

  • Массивы - специальный вид объектов со всеми вытекающими особенностями.
  • При копировании массива - его значение передается по ссылке, то же происходит и со вложенными в него массивами и ссылочными типами данных, поэтому для создания независимого массива его нужно клонировать специальными способами.
  • Значительно производительнее добавлять и удалять элементы в конец массива, чем в его начало или в любой другой участок.
  • Прямое указание длинны массива или лишняя запятая при перечислении элементов создадут разряженный массив, который не будет оптимален по расходу памяти и производительности.
  • Перебирать массивы быстрее с помощью цикла for…of…, чем любым другим способом.
  • Массиву можно добавить кастомное свойство, но тогда он превратится в псевдомассив и движок JavaScript не сможет так хорошо оптимизировать работу с ним.

Доступные курсы по Разработке и DevOps

Гарантия возврата денег — 30 дней
Неограниченный доступ
Сертификат об окончании
Доступные курсы по Разработке и DevOps