Что такое Array.apply

Прочитав этот SO-вопрос, я все еще немного запутался относительно того, что делает Array.apply. Рассмотрим следующий фрагмент:

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

Я ожидаю, что это приведет к инициализации массива с 5 неопределенными записями, а затем отобразит их, создав двухмерный массив из 5x5);

Вместо этого я просто получаю массив, как если бы он никогда не сопоставлялся:

[undefined, undefined, undefined, undefined, undefined]

Когда я завершаю вызов конструктора в массив в вызове Array.apply, тогда переходим к нему, он работает так, как ожидалось:

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

в результате чего;

[[undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined],
 [undefined, undefined, undefined, undefined, undefined]];

Какая сделка? Является ли Array.apply другим способом вызова нового массива Array() или Array.prototype.constructor? Есть ли другие ситуации, когда это было бы выгодно? Кроме того, почему мой первый подход не зашел на карту, которую я отправлял?

Благодарю! -Neil

Ответ 1

Хороший вопрос!

Функция конструктора Array (with может использоваться без new), когда передано более 1 аргумента, создает массив, содержащий аргументы, переданные в качестве его элементов. Так что вы можете сделать это:

Array(1, 2, 3); // => [1, 2, 3]

Как вы, вероятно, знаете, Function.prototype.apply позволяет вам предоставлять аргументы функции в виде массива. Таким образом, вызов Array.apply (который просто наследует свой .apply() от прототипа Function; любая функция, которая является экземпляром Function, позволит вам сделать это), что будет функциональностью, эквивалентной приведенному выше коду:

Array.apply(null, [1, 2, 3]); // => [1, 2, 3]

Теперь, вот почему все немного сбивает с толку. Метод Array.prototype.map определен таким образом, что он специально работает с разреженными массивами. Разреженный массив - это массив, длина которого превышает количество фактически вставленных элементов. Например:

var arr = [];
arr[0] = 'foo';
arr[5] = 'bar';

Построенный выше массив будет иметь свойство length 6, потому что он имеет элемент с индексом 0 и один с индексом 5. Однако, поскольку между этими индексами не было вставлено ни одного элемента, если вы вызовете map для него, вы увидите, что Функция отображения не применяется к отсутствующим элементам:

// This should throw, right? Since elements 1 through 4 are undefined?
var lengths = arr.map(function(s) { return s.length; });

// Nope!
lengths; // => [3, , , , , 3]

И почему мы видим такое поведение в вашем примере с new Array(5)? Вы догадались: потому что конструктор массива, когда ему передается один аргумент, создает разреженный массив с указанной длиной.

Таким образом, проблема заключается в том, что хотя map (и другие методы Array.prototype, такие как forEach) ведут себя особенно в отношении разреженных массивов, пропуская пропущенные значения, метод Function.prototype.apply не имеет такого особого поведения.

Ответ 2

Это очень интересный пример. И довольно хороший ответ Дана Тао. Но я думаю, что могу дать небольшое дополнительное объяснение.

В первом случае

новый массив (5)

создает пустой объект, затем передает функцию throw и дает длину 5. Из-за отсутствия каких-либо других аргументов этот объект не получит выделенных записей.

// Array(5) [ <5 empty slots> ]

И когда вы пытаетесь "сопоставить" эти записи, на самом деле ничего не происходит из-за отсутствия реальных записей.

Однако, если вы попытаетесь после этого шага, например, "array [0]", он вернет "undefined"...

В следующем случае вы используете метод "Вызов" функции Array() после первого "new Array (5)" (но на самом деле он не имеет разностного режима "Вызов" или "Construct" для использования с Array()).

Array.apply(null, new Array (5))

Таким образом, "new Array (5)" уже выдан как результат Array (5) [<5 empty slots>] и "Function.prototype.apply()" разбивает этот массив на пять параметров, в которые входит функция Array(). текущий шаг мы получаем:

// Array(5) [ undefined, undefined, undefined, undefined, undefined ]

Это пять реальных записей. И мы можем сделать "map()" с ними. Но есть небольшая ошибка в вашем результате, потому что мы в настоящее время получаем после

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

немного другой результат

/*
[…]    
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> ]
*/

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

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

это вернет массив "пять на пять, неопределенный".

/*
[…] 
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, … ]
*/

Но я говорю о том, что не только функция apply() имеет текущее поведение для разложения массива без реальных записей. Я приведу вам пример:

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

это фактически даст нам точно такой же результат - пять на пять не определено.

Если взглянуть поближе, то в первом случае (новая Array (5)) функция "Array()" получит пустой объект, потому что она работает в режиме "Construct" и имеет один параметр (5), но во втором действии

Array.apply() || Array (...)

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

Это из-за "apply()" или "..." поведения разлагаемых массивов. Когда он видит длину заданного массива, он автоматически преобразует "пустые слоты" в неопределенные значения, независимо от того, назначает ли это "apply()" или оператор распространения.

Ссылка от Ecma-262/6.0

(http://www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.apply)

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.

Здесь в предложении '8a' мы получаем "0", "1"... indexNames для передачи их в "8b" в массивоподобный аргумент - array ["0"], array ["1"]... и когда каждый элемент запрашивается возвращает значение "неопределенное", когда мы пытаемся получить значения из возвращенного массива из "нового массива (5)", который передается в функцию apply(). Таким образом, "CreateListFromArrayLike" заканчивается пятью аргументами undefined и применяет их в качестве параметров к функции Array().

Вместо этого оператор Spread использует итерационный протокол, но на практике в этом случае он действует одновременно.