JS: Коллекции
Теория: Массив
Массив — это самая распространенная структура данных во всех языках программирования. В этом уроке мы узнаем, что такое массив и объект, а также подробно разберем встроенные методы для работы с массивами в JavaScript.
Что такое массив
Массив — это структура данных, которая содержит упорядоченный набор элементов и предоставляет произвольный доступ к своим элементам.
Рассмотрим пример массива:
У нас есть массив a, и у него длина n. Он проиндексирован — каждое его значение хранится под определенным номером. Они идут по порядку и начинаются с нуля. Первый индекс всегда нуль — это стандарт.
Последний индекс будет n-1, так как индексация начинается с нуля, поэтому последний индекс на единицу меньше количества элементов.
Чтобы обратиться к элементам, нужно указать его имя и в квадратных скобках — индекс.
На изображении видно, что элементы идут подряд — это верное представление с точки зрения работы с массивом, но его реализация может отличаться. То, как физически массив хранится в памяти, зависит от языка программирования, там порядок набора элементов может быть другой. Но эти детали скрыты от разработчика, поэтому мы всегда представляем массив в виде элементов, которые идут друг за другом.
Определение массива в JavaScript
В JavaScript пустой массив создается с помощью квадратных скобок:
Если массив инициализируется какими-то значениями, то их нужно указать в квадратных скобках через запятую:
В JavaScript и других динамических языках нет ограничений на типы этих значений. Здесь можно смешивать любые типы. При этом элементами массива могут быть сами массивы.
Чтобы обратиться к элементу, нужно после имени массива написать квадратные скобки и индекс:
В итоге мы получим значения, с которыми можем работать дальше.
Также мы можем запросить элемент, которого нет в массиве. Например, используем индекс 4:
У нас нет элемента с таким индексом. В результате мы не получим ошибку. В этом случае в JavaScript мы получаем undefined.
Что такое объект
В работе с массивами в JavaScript есть особенность:
С помощью операции typeof мы смотрим, какой тип у массива. Получаем object.
Дело в том, что когда мы создаем массив, то создается объект. Квадратные скобки заменяют такую конструкцию:
Фактически мы создаем новый элемент с типом array, в котором через запятую перечисляем элементы внутри него, и он является объектом.
Array — это подтип объекта, который обладает определенными свойствами. Мы можем обращаться к нему, как к другим объектам. Например, array определяет свойство length, которое возвращает длину этого массива:
Свойство length обновляется автоматически. Например, если мы что-то добавляем или удаляем, то оно будет меняться в зависимости от того, что мы делаем.
При этом квадратные скобки при обращении к массиву не являются специфичными для массива. В примере мы обратились к свойству через квадратные скобки arr2['length'] — это работает. Также к свойствам можно обращаться через точку arr2.length. Оба варианта делают одно и то же.
Теперь представим, как бы мы определили массив в терминах объекта. Мы сделали объект, у которого ключи являются числовыми — как индексы:
Этот объект по своему виду соответствует массиву, который разобрали выше. Используются те же значения.
Если обратиться к нему также через скобки и поставить туда индекс 0, то мы получим единицу. Это работает практически как массив. Массив синтаксически ничего не добавляет кроме конструкции [], которая заменяет new Array(). Все остальное — это часть типа данных object в JavaScript. Из-за того, что они смешиваются в одну сущность, это добавляет некоторые неожиданные эффекты, с которыми мы познакомимся позже.
В большинстве языков программирования массивы так не пересекаются с объектами, так как являются разными типами данных. Например, в PHP вообще нет массивов, а есть только объекты или так называемые ассоциативные массивы, с которыми мы тоже познакомимся попозже.
Что можно делать с массивом
Рассмотрим манипуляции, которые можно делать с массивом в JavaScript:
- Обновление
- Произвольное добавление
- Последовательное добавление
- Удаление
- Добавление и удаление элементов в начале или конце массива
- Итерация
- Функции высшего порядка
- Другие функции
Разберем каждый метод подробнее.
Обновление
Мы можем переустановить значение любого элемента в массиве. Для этого обращаемся к соответствующему индексу и приравниваем его:
Синтаксис такой же, как и с константами переменными. Мы просто присваиваем новое значение в индекс массива arr[0] = 'hello'. Если мы потом выведем его, то увидим новое значение hello. При этом длина массива не изменилась, так как мы просто заменили значение под индексом 0.
В данном примере используется const, а не let. Мы сохраняем в константе массив, и константа не меняется. Мы всегда работаем через константу с одним и тем же массивом.
При этом JavaScript не гарантирует, что сам массив не будет меняться. Мы можем изменить его добавлением, удалением или заменой каких-то элементов. Но сам массив остается тот же. Поэтому, если у нас не происходит замены всего массива на новый, то нужно всегда использовать const. Это применимо и к другим типам данных.
Напомним, что const пишется один раз при создании массива.
Произвольное добавление
Также мы можем делать произвольное добавление. Нам необязательно добавлять значение в тот индекс, который уже существует. Например:
Здесь мы добавляем значение не в последний индекс. Сейчас их у нас три, а мы добавляем значение world в индекс 5.
В этом случае мы не получим ошибку. Если распечатать массив, то можно увидеть следующее:
Если мы выведем длину массива, то она будет равна 6:
Получается, что пустые места — это реальные элементы — чем-то заполнились. Если мы попытаемся вывести на экран значение под индексом 4, то получим undefined:
Сам элемент существует, но его значение равно undefined. Если мы обращаемся к индексу, которого не существует, то тоже получаем undefined.
Получается, что в одном случае индекс с элементом есть, а в другом случае индекса нет. Но мы в любом случае получаем undefined.
Иногда разница в этих значениях есть, и ее нужно учитывать. А иногда ее нет. Главное помнить, что undefined не всегда означает, что этого индекса не существует. Там действительно может лежать значение undefined.
Последовательное добавление
Чтобы добавить что-то в массив, используют метод push:
push берет последний индекс, прибавляет к нему единицу и размещает по этому индексу то значение, которое мы передаем в push.
Если распечатать его, то мы получим 10:
Это стандартный подход при работе с массивом.
Удаление
При работе с массивами есть механизм удаления элемента, но он работает не так, как ожидается:
Вместо удаления элемента происходит его очистка, а значением элемента становится undefined.
Если распечатать, то увидим пропущенное значение:
Удалять что-то из массива — это плохая практика, которая может приводить к проблемам. О них мы поговорим позже. Поэтому так не стоит делать.
Если вам нужно получить массив, в котором отсутствуют какие-то элементы, то нужно применять фильтрацию. По факту нужно сформировать новый массив и добавить в него только те элементы из оригинального массива, которые нам нужны.
Добавление и удаление элементов в начало или конец массива
Чтобы добавить или удалить элементы в начало или конец массива, используются методы shift, unshift, pop и push. Эта группа методов позволяет работать с массивом как с очередью — метод доступа FIFO, или стеком — метод доступа LIFO. Рассмотрим это на примере:
Очередь является упорядоченным набором данных, организованным по принципу FIFO — first in — first out. То есть элементы всегда добавляются в конец очереди, а удаляются из ее начала:
Стек является упорядоченным набором данных, организованным по принципу LIFO — last in — first out. То есть элементы добавляются и удаляются всегда из конца такой коллекции:
Итерация
Классический способ итерации для императивных языков — это цикл. В JavaScript много циклов, для работы с массивами часто применяется for…of:
Функции высшего порядка
Канонический способ работы с коллекциями в JavaScript — функции высшего порядка.
Представим, что у нас есть массив numbers:
Мы хотим выполнить с ним стандартные операции: map, filter, reduce и reduceRight:
Пример работы с map:
То же самое:
В JavaScript все функции являются объектами первого рода, и мы можем их передавать как аргументы. Во втором примере мы вызываем метод map на массиве numbers и передаем туда саму функцию Math.sqrt(), так как она принимает тот же параметр, что и наша функция-обертка.
В итоге получаются корни — roots. Если их распечатать, у нас появляется 1, 2, 3.
Следующий пример — это фильтрация:
Если мы распечатаем получившийся массив, то получим 4,9, так как у нас единица меньше 3, она сюда не попадает.
Наиболее комплексный пример из функций высшего порядка — это reduce:
В итоге возвращаем тело функции в преобразованный новый аккумулятор и считаем сумму элементов массива. Если его распечатать, то получаем 14.
У анонимной функции достаточно много параметров. В JavaScript это сделано для удобства. Чаще всего используются только первые два параметра. Список параметров:
acc— аккумуляторvalue— значение текущего, которое обрабатываетсяindex— индекс, что может быть полезно в определенных случаяхarr— сам массив
Пример работы с reduceRight:
Другие функции
Чтобы строки превратить в массив, а также сделать обратное, используют split и join:
Поиск в массиве и проверка соблюдения условий элементами массива производится с помощью find, findIndex, indexOf, lastIndexOf, some, every, includes и так далее. Например:
Сортировка массива происходит с помощью sort, reverse.
Разберем еще один метод flat(). Он делает содержимое вложенных элементов-массивов исходного массива элементами самого исходного массива. Он словно выравнивает массив. Посмотрим на код:
Как видно из предыдущего примера, нам удалось выровнять все элементы на один уровень ниже лежащих массивов. При этом элементы более глубоко лежащих массивов остались невыровненными.
Можно управлять уровнем глубины выравнивания, который указывается в параметре метода:
Библиотека lodash
Существуют библиотеки, которые позволяют пользоваться множеством готовых функций. Одной из наиболее актуальных на текущей момент Javascript-библиотек является lodash. В ней реализованы различные функции по работе с массивами, коллекциями, строками и другими объектами языка.
Эта библиотека предоставляет большое количество функций высшего порядка, которые поддерживают функциональный стиль программирования. Подробнее ознакомиться со всеми возможностями lodash можно на сайте библиотеки.
Изменяемость VS Неизменяемость
Изменить объект в императивном стиле, когда можно писать код в неизменяемом — часто является не самым удачным решением. Это ведет к изменению состояния программы и усложняет ее. Поэтому, когда изучаете методы, всегда обращайте внимание, изменяют ли они исходный массив или нет.
Например, большинство примененных к массиву функций высшего порядка результатом возвращают новый массив. При этом они оставляют исходный нетронутым. Метод sort, как сказано в документации, необратимым образом модифицирует исходный массив. Поэтому такую сортировку лучше сделать на копии массива, если нам не нужно, чтобы исходный массив менялся.
Возьмем для примера типичную ситуацию, когда необходимо добавить новый элемент в массив. Рассмотрим, как справиться с этой задачей на примeре изменяющего метода push и неизменяющего concat:
push
push добавляет один или более элементов в конец массива, тем самым изменяет его, и возвращает новую длину массива.
concat
concat возвращает новый массив, который содержит элементы исходного массива и элементы, переданные в качестве аргументов.
Выводы
В этом уроке мы узнали, что такое массив. Это структура данных, которая содержит упорядоченный набор элементов и предоставляет произвольный доступ к своим элементам. Также мы рассмотрели манипуляции, которые можно делать с массивом в JavaScript. К ним относятся:
- Обновление
- Произвольное добавление
- Последовательное добавление
- Удаление
- Добавление и удаление элементов в начале или конце массива
- Итерация
- Функции высшего порядка
- Другие функции
