Хабрахабр

Defined or Undefined? Нюансы создания массивов в JavaScript

image

Пару месяцев назад я наткнулся на интересный вопрос на stackoverflow, там, если вкратце, человек хотел создать пустую матрицу 5х5, и, используя один способ у него получилось, а используя другой — нет. В развернувшейся дискуссии на этот счёт были приведены интересные мысли.

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

Как же мы это будем делать? В общем так, перед нами стоит задача создать массив. Как ни странно, есть разные варианты, в зависимости от того, что мы хотим получить.

Если мы используем ключевое слово new, то используется метод Construct, который создаёт новый экземпляр объекта, присваивает ему ссылку this , и, затем, выполняет тело функции. Нам известно, что функции в JavaScript имеют два внутренних метода Call и Construct. Не все функции имеют данный метод, но нам это сейчас не так уж и важно.

При создании же массивов есть одна особенность: не важно, используем мы Array(…) или new Array(…) — спецификация ECMAScript не делает различий для них и, кроме того, считает их эквивалентными.

1. 22. When called as a constructor it creates and initializes a new exotic Array object. 1 The Array Constructor
The Array constructor is the %Array% intrinsic object and the initial value of the Array property of the global object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments.
When Array is called as a function rather than as a constructor, it also creates and initializes a new Array object.

Поэтому, и я не буду мудорствовать лукаво, и, в примерах буду использовать только конструкцию new Array(…), дабы не сбивать никого с толку.

Начнём.

Создаём массив:

let arr = new Array(5);

Что же у нас получилось?

console.log(arr); // Array(5) [ <5 empty slots> ]
console.log(arr[0]); // undefined
console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined

Хм… ну, в принципе, так ведь и должно быть — мы задали длину и получили пять пустых ячеек, со значением undefined, с которыми можно работать дальше, верно? Правда, есть тут пара моментов, которые меня смущают. Давайте проверим.

let arr = new Array(5).map(function() ); console.log(arr); // Array(5) [ <5 empty slots> ]
console.log(arr[0]); // undefined
console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined
console.log(arr[0][0]); // TypeError: arr[0] is undefined

Как же так, ведь мы должны были получить матрицу, и в каждой ячейке, соответственно, должен быть массив из 5 элементов…

Обратимся опять же к документации ECMAScript и посмотрим, что в ней написано касательно метода создания массивов с одним аргументом:

1. 22. 2 Array (len)
This description applies if and only if the Array constructor is called with exactly one argument.
1.

Let numberOfArgs be the number of arguments passed to this function call.
2. 1. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
4. Assert: numberOfArgs = 1.
3. ReturnIfAbrupt(proto).
6. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%").
5. If Type(len) is not Number, then
1. Let array be ArrayCreate(0, proto).
7. Assert: defineStatus is true.
3. Let defineStatus be CreateDataProperty(array, "0", len).
2. Else,
1. Let intLen be 1.
8. If intLen ≠ len, throw a RangeError exception.
9. Let intLen be ToUint32(len).
2. Assert: setStatus is not an abrupt completion.
11. Let setStatus be Set(array, "length", intLen, true).
10. Return array.

Про них ни слова… То есть длина == 5 есть, а пяти ячеек нет. И, что мы видим, оказывается объект создан, свойство length создано в процедуре ArrayCreate(6 пункт), значение в свойстве length проставлено (пункт 9), а что с ячейками? Да, компилятор путает нас, когда мы пытаемся обратится к отдельной ячейке, он выдаёт, что её значение undefined, тогда как её фактически нет.

Вот, для сравнения метод создания массивов с несколькими аргументами отправленными в конструктор:

1. 22. 3 Array (...items )
This description applies if and only if the Array constructor is called with at least two arguments.
When the Array function is called the following steps are taken:
1.

Let numberOfArgs be the number of arguments passed to this function call.
2. 1. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
4. Assert: numberOfArgs ≥ 2.
3. ReturnIfAbrupt(proto).
6. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%").
5. ReturnIfAbrupt(array).
8. Let array be ArrayCreate(numberOfArgs, proto).
7. Let items be a zero-origined List containing the argument items in order.
10. Let k be 0.
9. Let Pk be ToString(k).
2. Repeat, while k < numberOfArgs
1. Let defineStatus be CreateDataProperty(array, Pk, itemK).
4. Let itemK be items[k].
3. Increase k by 1.
11. Assert: defineStatus is true.
5. Return array. Assert: the value of array’s length property is numberOfArgs.
12.

Здесь, пожалуйста — 10 пункт, создание тех самых ячеек.

На помощь нам придет Function.prototype.apply()!
Давайте сразу проверим её в действии: Итак, как работает конструктор массивов мы разобрались, но задача осталась по прежнему не решенной, ибо матрица не построена.

let arr = Array.apply(null, new Array(5)); console.log(arr); // Array(5) [ undefined, undefined, undefined, undefined, undefined ]
console.log(arr[0]); // undefined
console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true }

Ура, здесь отчетливо наблюдаются все пять ячеек, а также у первой, тестовой, ячейки под номером “0” появился дескриптор.

В данном случае программа работала следующим образом:

  1. Мы вызвали метод Function.prototype.apply() и передали ему контекст null, а в качестве массива new Array(5).
  2. new Array(5) создал массив без ячеек, но с длиной 5.
  3. Function.prototype.apply() использовала внутренний метод разбития массива на отдельные аргументы, в результате чего, передала конструктору Array пять аргументов со значениями undefined.
  4. Array получив 5 аргументов со значениями undefined, добавил их в соответствующие ячейки.

Всё вроде понятно, кроме того, что же это за внутренний метод у Function.prototype.apply(), который из ничего делает 5 аргументов — предлагаю опять взглянуть на документацию ECMAScript:

2. 19. 1 Function.prototype.apply 3.

If IsCallable(func) is false, throw a TypeError exception.
2. 1. Let argList be CreateListFromArrayLike(argArray). If argArray is null or undefined, then Return Call(func, thisArg).
3.

3. 7. 17 CreateListFromArrayLike (obj [, elementTypes] )

ReturnIfAbrupt(obj).
2. 1. If Type(obj) is not Object, throw a TypeError exception.
4. If elementTypes was not passed, let elementTypes be (Undefined, Null, Boolean, String, Symbol, Number, Object).
3. ReturnIfAbrupt(len).
6. Let len be ToLength(Get(obj, "length")).
5. Let index be 0.
8. Let list be an empty List.
7. Let indexName be ToString(index).
b. Repeat while index < len
a. ReturnIfAbrupt(next).
d. Let next be Get(obj, indexName).
c. Append next as the last element of list.
f. If Type(next) is not an element of elementTypes, throw a TypeError exception.
e. Return list. Set index to index + 1.
9.

Смотрим самые интересные пункты:

2. 19. 1 — пункт 3: создание списка аргументов из объекта подобного массиву (как мы помним у таких объектов должно быть свойство длины). 3.

3. 7. В нём идёт проверка на то, объект это или нет, и, если да, запрос значения поля length (пункт 4). 17 — непосредственно сам метод создания списка. Создаётся цикл с инкрементацией индекса до значения взятого из поля length (пункт 8). Затем создается индекс, равный “0” (пункт 7). А как мы помним, при обращении к значению отдельной ячейки массива в котором фактически нет ячеек всё равно выдаёт значение — undefined. В этом цикле идёт обращение к значениям ячеек переданного массива с соответствующими индексами (пункт 8a и 8b). Полученное значение добавляется в конец списка аргументов (пункт 8e).

Ну, а теперь, когда, всё встало на свои места, можно спокойно построить уже ту самую пустую матрицу.

let arr = Array.apply(null, new Array(5)).map(function(){ return Array.apply(null,new Array(5)); }); console.log(arr); // Array(5) [ (5) […], (5) […], (5) […], (5) […], (5) […] ]
console.log(arr[0]); // Array(5) [ undefined, undefined, undefined, undefined, undefined ]
console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: (5) […], writable: true, enumerable: true, configurable: true }
console.log(arr[0][0]); // undefined
console.log(Object.getOwnPropertyDescriptor(arr[0],"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true }

Теперь, как можно заметить, всё сходится и довольно просто выглядит: мы, известным нам уже способом, создаём простой пустой Array.apply(null, new Array(5)) массив а затем передаём его методу map, который создаёт по такому же массиву в каждой из ячеек.

В ECMAScript6 появился оператор spread, и, что характерно, он также специфически работает с массивами. Кроме того, можно сделать ещё проще. Поэтому, мы можем просто вбить:

let arr = new Array(...new Array(5)).map(() => new Array(...new Array(5)));

или уж совсем упростим, хоть я ранее и обещал new не трогать…

let arr = Array(...Array(5)).map(() => Array(...Array(5)));

прим.: здесь мы также использовали стрелочные функции, так как раз мы всё равно имеем дело со spread оператором, который появился в той же спецификации, что и они.

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

Ну, и, конечно, так просто быстрее и удобнее. Кроме того, мы, естественно, можем построить свои функции, которые подобным Function.prototype.apply() перебором, будут создавать для нас нормальные массивы с пустыми ячейками, однако же понимание внутренних принципов работы JavaScript и, соответственно с этим, правильное и адекватное использование встроенных функций, является базисом, освоить который приоритетно.

И, напоследок, возвращаясь к тому самому вопросу на stackoverflow – там, я напомню, человек ошибочно посчитал, что полученный им метод привёл к правильному ответу, и, что он получил матрицу 5х5, однако — там закралась маленькая ошибка.

Он вбил:

Array.apply(null, new Array(5)).map(function(){
return new Array(5);
});

Как думаете, какой здесь будет на самом деле результат?

Ответ

console.log(arr); // Array(5) [ (5) […], (5) […], (5) […], (5) […], (5) […] ]
console.log(arr[0]); // Array(5) [ <5 empty slots> ]
console.log(Object.getOwnPropertyDescriptor(arr,«0»)); // Object { value: (5) […], writable: true, enumerable: true, configurable: true }
console.log(arr[0][0]); // undefined
console.log(Object.getOwnPropertyDescriptor(arr[0],«0»)); // undefined

неправда ли, это не совсем то, что он хотел получить…

Ссылки:

→ ECMAScript 2015 Language Specification
→ What is Array.apply actually doing

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть