Первый шаг к рабочему и эффективному коду — разобраться в механизмах, которые лежат в основе языка. Например, некоторые функции в JavaScript не всегда ведут себя так, как того ожидает программист. Рассмотрим один из таких примеров.
Сценарий
Для более наглядной демонстрации предположим, что вам нужно сгенерировать массив чисел от 0 до 99. Как можно это сделать? Например, так:
const arr = [];
for (let i = 0; i < 100; i++) {
arr[i] = i;
}
Возможно, цикл for
в JavaScript заставляет вас чувствовать себя неуютно. На самом деле, многие программисты годами не используют for
-циклы благодаря функциям forEach()
, map()
, filter()
, и reduce()
. Декларативное функциональное программирование рулит!
Если вы еще не поддались массовому помешательству на почве функционального программирования, решение выше кажется вам абсолютно нормальным. Технически так и есть, но распробовав магию функций высшего порядка, вы, скорее всего, подумаете: «Должен быть способ получше».
Тогда первая реакция на проблему может быть такой: «Знаю! Я создам пустой массив длиной 100 и присвою каждому элементу значение его индекса с помощью map()
!». JavaScript позволяет создать пустой массив длиной n с помощью функции-конструктора:
const arr = Array(100);
.
Объяснение
Нужно обозначить один важный технический момент, чтобы объяснить, как так вышло. По своей сути массивы в JavaScript — объекты с числами в роли индексов. Например:
['a', 'b', 'c']
эквивалентно объекту
{ 0: 'a', 1: 'b', 2: 'c', length: 3 }
Пытаясь обратиться к элементу с индексом 0, вы на самом деле обращаетесь к параметру объекта с ключом 0. Это важно, потому что учитывая то, что массив — это объект, и то, как выполняются функции высшего порядка, мы получаем вполне понятную проблему.
Создавая массив с помощью функции-конструктора, вы получаете объект-массив, у которого параметрlength
равен числу, которое вы задали, но больше в объекте ничего нет. В нем нет индексов.{ // нет индексов! length: 100 }
Вы получаете
undefined
, пытаясь обратиться к элементу массива с индексом 0, но это не потому, что в элементе с индексом 0 хранитсяundefined
, а потому, что JavaScript всегда возвращаетundefined
при попытке обратиться к несуществующему элементу массива.
Так устроено, что функции высшего порядка —map()
,reduce()
,filter()
иforEach()
— проходятся по индексам от 0 до значенияlength
, но callback-функция вызывается только если в объекте существуют индексы. Это объясняет, почему функция ничего не возвращает, и почему ничего не происходит, когда мы используем функциюmap()
на массив, в котором нет индексов.Решение
Теперь легко сделать вывод, что нам нужен массив, чья внутренняя структура содержит индексы для каждого элемента от 0 до
length
. Лучший способ сделать это — деструктурировать массив в пустой массив.const arr = [...Array(100)].map((_, i) => i); console.log(arr[0]); // 0
Деструктуризация массива в пустой массив позволяет получить массив, содержащий
undefined
в каждом элементе.{ 0: undefined, 1: undefined, 2: undefined, ... 99: undefined, length: 100 }
Так происходит потому, что оператор деструктуризации проще, чем функция
map()
. Он просто проходит по массиву (или любому итерируемому объекту) от 0 доlength
и создает во внешнем массиве новый индекс, содержащий значение, полученное из элемента деструктурированного массива с тем же индексом. JavaScript возвращаетundefined
при обращении к любому элементу деструктурированного массива (так происходит, потому что в нем отсутствуют элементы и индексы), поэтому мы получаем новый массив, заполненный индексами, и потому доступный для функцииmap()
(а такжеreduce()
,filter()
иforEach()
).Заключение
Таким образом, понимая внутреннее представление массивов в JavaScript, можно создавать массивы любой длины, с любым нужным значением элементов.
Смотрите также: Фундаментальные принципы объектно-ориентированного программирования на JavaScript
Перевод статьи «Here’s Why Mapping a Constructed Array in JavaScript Doesn’t Work»