image

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

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

Прим.: я также ответил и под той дискуссией, под ником AndreyGS – там я ответил довольно кратко, здесь же я постараюсь до конца охватить проблему.

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

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

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

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


Поэтому, и я не буду мудрствовать лукаво, и, в примерах буду использовать только конструкцию 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() { return new Array(5); });

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 и посмотрим, что в ней написано касательно метода создания массивов с одним аргументом:

22.1.1.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. Assert: numberOfArgs = 1.
3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%").
5. ReturnIfAbrupt(proto).
6. Let array be ArrayCreate(0, proto).
7. If Type(len) is not Number, then
1. Let defineStatus be CreateDataProperty(array, "0", len).
2. Assert: defineStatus is true.
3. Let intLen be 1.
8. Else,
1. Let intLen be ToUint32(len).
2. If intLen ? len, throw a RangeError exception.
9. Let setStatus be Set(array, "length", intLen, true).
10. Assert: setStatus is not an abrupt completion.
11. Return array.


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

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

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


Здесь, пожалуйста — 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:

19.2.3.1 Function.prototype.apply

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

7.3.17 CreateListFromArrayLike (obj [, elementTypes] )

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


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

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

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

Комментарии (24)


  1. napa3um
    09.08.2019 02:09

    Прост для полноты положу тут, как красиво создавать заполненные (неразрежённые) массивы без грязных хаков с апплаями:

    const matrix = new Array(5).fill(
      new Array(5).fill()
    )


    1. qbz
      09.08.2019 02:17

      По спеке вроде не надо нью у Array конструктора — поведение одинаковое — что без, что с.
      Еще проще:


      [...Array(5)].fill([...Array(5)])


      1. TheShock
        09.08.2019 02:28

        Ваш код — тоже некорректный:


        1. qbz
          09.08.2019 02:48

          Ага, согласен, ну тогда:


          [...Array(5)].map(() => [...Array(5)])


          1. ukbpyh Автор
            09.08.2019 11:37

            Этот код аналогичен, тому, что в статье.

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

            Просто вы использовали способ создания массива, через литерал, а не функцию.
            Но в целом, согласен — про литералы я сам в посте не упомянул и ваше дополнение вполне уместно.


            1. xGromMx
              09.08.2019 22:46
              +1

              куда проще

              Array.from({length: 5}, () => Array.from({length: 5}))
              


            1. xGromMx
              09.08.2019 22:49
              +2

              А вообще я когда-то давно сделал такой гист
              gist.github.com/xgrommx/a25ffa3a7753e01ee679


    1. TheShock
      09.08.2019 02:27

      Прост для полноты положу тут, как красиво создавать заполненные (неразрежённые) массивы без грязных хаков с апплаями:
      const matrix = new Array(5).fill(
        new Array(5).fill()
      )


      Ваш код — некорректный:


      1. napa3um
        09.08.2019 03:23

        Да, согласен, так получится заполнять только примитивными значениями массивы, зря я замахнулся на матрицы, пусть останется только одномерный случай и без нью :). Для матриц придётся маппить (что уже не так компактно, но всё ещё прозрачнее хаков с эпплаями, и fill красивее, имхо, чем вариант со спредом у комментатора выше :))

        const matrix = Array(5).fill().map(
          () => Array(5).fill()
        )


        1. diomas
          09.08.2019 11:10
          +1

          так тоже будет ожидаемый результат:


          const matrix = Array.from(Array(5), () => Array(5).fill())


          1. ukbpyh Автор
            09.08.2019 11:53

            Спасибо всем, кто дополнительно привел свои варианты и тем кто нашел в части из них ошибки — ваши комментарии прекрасно дополняют материал.


    1. ukbpyh Автор
      09.08.2019 12:51

      Прост для полноты положу тут, как красиво создавать заполненные (неразрежённые) массивы без грязных хаков с апплаями:

      const matrix = new Array(5).fill(
        new Array(5).fill()
      )

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

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


  1. BerkutEagle
    09.08.2019 06:06

    Мне кажется, в любом учебнике по JS написано, что

    const arr = new Array(5)
    создаёт массив без элементов, но с заданной длиной.
    Это будет эквивалентно такому коду:
    const arr = [];
    arr.length = 5;


    1. ukbpyh Автор
      09.08.2019 11:19

      Да, согласен, написано. И, правильно, что вы заострили своё внимание на этом моменте — это лишнее напоминание о том, что для начала надо читать инструкцию по применению.
      Однако, прочитав определение в учебнике, мы не всегда сразу можем увидеть из этого все возможные следствия, и, если, к тому же, мы не знаем в точности их внутреннюю составляющую, самостоятельно не обращаясь к спецификациям. И, этот пост создавался, в том числе, для того, чтобы подчеркнуть важность понимания внутренних механизмов языка на котором мы пишем.


  1. Aingis
    09.08.2019 13:13

    Не проще использовать специально сделанный для этого Array.from? Второй аргумент — как раз функция маппинга.

    let arr = Array.from(Array(5), () => new Array(5).fill());

    А так, про дырки вопрос известный. Для лучшей производительности лучше заполнить массивы данными того типа, что будут использоваться. Хотя бы нулями.


    1. ukbpyh Автор
      09.08.2019 13:39
      +1

      diomas выше писал:

      так тоже будет ожидаемый результат:

      const matrix = Array.from(Array(5), () => Array(5).fill())

      Ваш код аналогичен.

      Кроме того, касательно Array.from() — я уже писал в комментарии выше — этот метод работает только в ECMAScript 6 и до сих пор не имеет поддержки в некоторых браузерах (Array.from()), метод же Function.prototype.apply() единственный, который может использоваться везде или практически везде, но прежде всего целью статьи было, показать через внутренний принцип работы некоторых функций то как JavaScript работает с массивами и подобными им объектами.
      Оператор spread, в частности, был, приведен в статье в качестве примера, в том числе, для того, чтобы показать, что не только Function.prototype.apply(), может проводить подобные фокусы. Использование других методов вроде from() и fill() конечно вполне себе законно и расширяет возможности, и где-то красоту, поэтому я, естественно согласен, что их можно и нужно использовать в соответствующих ситуациях.

      На счет производительности думаю вы правы, но мне кажется этот вопрос выходит за рамки данной статьи. Но в качестве «справочно» сойдёт :)


      1. Aingis
        09.08.2019 16:00
        +1

        ECMAScript 6 — это ECMAScript 2015, то есть ему уже 4 года. Да и полифилится Array.from() легко. На чистом ES5 сейчас пишут только откровенные ретрограды.


        1. ukbpyh Автор
          09.08.2019 16:45
          +1

          Array.from()

          Настольные
          Возможность Chrome Firefox (Gecko) Internet Explorer Opera Safari
          Базовая поддержка 45 32 (32) Нет Нет 9.0

          Мобильные
          Возможность Android Chrome для Android Firefox Mobile (Gecko) IE Mobile Opera Mobile Safari Mobile
          Базовая поддержка Нет Нет 32.0 (32) Нет Нет Нет

          И, я не призываю писать всё время на ES5, повторюсь: цель статьи была не в том, чтобы обязательно вывести в ней какой ультимативный способ для создания массивов/матриц, а для разъяснения принципов работы некоторых внутренних инструментов JavaScript. И ничего против использования Array.from(), Array.prototype.fill(), оператора spread и прочего, я не имею.


          1. Aingis
            09.08.2019 19:53
            +1

            Не смотрите на русский MDN — там во многом старая информация.

            Array.from() browser compatibility

            Для фич из новых версий ES куда удобнее ECMAScript Compatability Table (показывает только принципиально наличие, не гарантируя полностью правильную работу).


            1. ukbpyh Автор
              09.08.2019 20:09

              Спасибо, здесь вы правы, учту.


  1. vitvad
    10.08.2019 01:47

    проблема этого кода

    let arr = new Array(5).map(function() { return new Array(5); });

    не в методе создания массива, а в методе `map`
    первый же абзац говорит что `map` не вызывает callback если значение в массиве не было инициализированно. Но про этом если присвоить элементу массива `undefined` это считается проинициализированным.

    var ar = new Array(5);ar[2] = void 0; ar.map((v,i) => console.log(v,i)) // undefined 2

    в итоге немного изменив код получим желаемый результат

    
    new Array(5).fill(void 0).map(() => new Array(5));
    


    1. ukbpyh Автор
      10.08.2019 01:57

      Получится

      /*
      […]
      0: Array(5) [ <5 empty slots> ]
      1: Array(5) [ <5 empty slots> ]
      2: Array(5) [ <5 empty slots> ]
      3: Array(5) [ <5 empty slots> ]
      4: Array(5) [ <5 empty slots> ]
      */

      а надо
      /*
      […]
      0: Array(5) [ undefined, undefined, undefined, … ]
      1: Array(5) [ undefined, undefined, undefined, … ]
      2: Array(5) [ undefined, undefined, undefined, … ]
      3: Array(5) [ undefined, undefined, undefined, … ]
      4: Array(5) [ undefined, undefined, undefined, … ]
      */

      дополнив то, что вы написали, этого можно достичь так:
      new Array(5).fill(void 0).map(() => new Array(5).fill(void 0));
      или, еще проще:
      new Array(5).fill().map(() => new Array(5).fill());

      впрочем, чуть выше napa3um уже привел подобный пример.


      1. faiwer
        10.08.2019 13:07
        +1

        Но всё же тащить void 0 в продакшн лучше не стоит :)


    1. qbz
      10.08.2019 02:51
      +1

      Зачем в 2019 писать void 0?