Создание диапазона в JavaScript - странный синтаксис

Я запустил следующий код в списке рассылки es-discuss:

Array.apply(null, { length: 5 }).map(Number.call, Number);

Это создает

[0, 1, 2, 3, 4]

Почему это результат кода? Что здесь происходит?

Ответ 1

Понимание этого "взлома" требует понимания нескольких вещей:

  • Почему мы не просто делаем Array(5).map(...)
  • Как Function.prototype.apply обрабатывает аргументы
  • Как Array обрабатывает несколько аргументов
  • Как функция Number обрабатывает аргументы
  • Что Function.prototype.call делает

Это довольно продвинутые темы в javascript, поэтому это будет больше, чем довольно долго. Мы начнем с вершины. Пристегнитесь!

1. Почему не просто Array(5).map?

Какой массив, действительно? Обычный объект, содержащий целые ключи, которые сопоставляются значениям. Он имеет другие специальные функции, например магическую переменную length, но на ней ядро, это обычная карта key => value, как и любой другой объект. Давайте немного поиграем с массивами, не так ли?

var arr = ['a', 'b', 'c'];
arr.hasOwnProperty(0); //true
arr[0]; //'a'
Object.keys(arr); //['0', '1', '2']
arr.length; //3, implies arr[3] === undefined

//we expand the array by 1 item
arr.length = 4;
arr[3]; //undefined
arr.hasOwnProperty(3); //false
Object.keys(arr); //['0', '1', '2']

Мы получаем неотъемлемую разницу между количеством элементов в массиве arr.length и количеством отображений key=>value, которые имеет массив, который может отличаться от arr.length.

Расширение массива с помощью arr.length не создает никаких новых сопоставлений key=>value, так что это не значит, что массив имеет значения undefined, у него нет этих ключей. И что происходит, когда вы пытаетесь получить доступ к несуществующей собственности? Вы получаете undefined.

Теперь мы можем немного поднять голову и посмотреть, почему функции, такие как arr.map, не перешагивают эти свойства. Если arr[3] был просто undefined, а ключ существовал, все эти функции массива просто переходили бы на него, как и любое другое значение:

//just to remind you
arr; //['a', 'b', 'c', undefined];
arr.length; //4
arr[4] = 'e';

arr; //['a', 'b', 'c', undefined, 'e'];
arr.length; //5
Object.keys(arr); //['0', '1', '2', '4']

arr.map(function (item) { return item.toUpperCase() });
//["A", "B", "C", undefined, "E"]

Я намеренно использовал вызов метода, чтобы еще раз доказать, что самого ключа никогда не было: вызов undefined.toUpperCase вызвал бы ошибку, но это не так. Чтобы доказать, что:

arr[5] = undefined;
arr; //["a", "b", "c", undefined, "e", undefined]
arr.hasOwnProperty(5); //true
arr.map(function (item) { return item.toUpperCase() });
//TypeError: Cannot call method 'toUpperCase' of undefined

И теперь мы догадались: как Array(N) делает вещи. Раздел 15.4.2.2 описывает процесс. Там куча mumbo jumbo нам все равно, но если вам удастся читать между строками (или вы можете просто доверять мне это, но не делать этого), это в основном сводится к следующему:

function Array(len) {
    var ret = [];
    ret.length = len;
    return ret;
}

(работает в соответствии с предположением (которое проверено в фактической спецификации), что len является допустимым uint32, а не просто любым значением)

Итак, теперь вы можете понять, почему выполнение Array(5).map(...) не работает - мы не определяем элементы len в массиве, мы не создаем сопоставления key => value, мы просто изменяем свойство length.

Теперь, когда у нас есть это, посмотрим на вторую магическую вещь:

2. Как работает Function.prototype.apply

Что apply делает, в основном принимает массив и разворачивает его как аргументы вызова функции. Это означает, что следующие примерно одинаковы:

function foo (a, b, c) {
    return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3

Теперь мы можем облегчить процесс наблюдения за тем, как работает apply, просто зарегистрировав специальную переменную arguments:

function log () {
    console.log(arguments);
}

log.apply(null, ['mary', 'had', 'a', 'little', 'lamb']);
 //["mary", "had", "a", "little", "lamb"]

//arguments is a pseudo-array itself, so we can use it as well
(function () {
    log.apply(null, arguments);
})('mary', 'had', 'a', 'little', 'lamb');
 //["mary", "had", "a", "little", "lamb"]

//a NodeList, like the one returned from DOM methods, is also a pseudo-array
log.apply(null, document.getElementsByTagName('script'));
 //[script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script]

//carefully look at the following two
log.apply(null, Array(5));
//[undefined, undefined, undefined, undefined, undefined]
//note that the above are not undefined keys - but the value undefined itself!

log.apply(null, {length : 5});
//[undefined, undefined, undefined, undefined, undefined]

Легко доказать мое утверждение во втором-последнем примере:

function ahaExclamationMark () {
    console.log(arguments.length);
    console.log(arguments.hasOwnProperty(0));
}

ahaExclamationMark.apply(null, Array(2)); //2, true

(да, каламбур). Отображение key => value, возможно, не было в массиве, который мы передали в apply, но он, безусловно, существует в переменной arguments. Это та же самая причина, по которой работает последний пример: ключи не существуют на передаваемом объекте, но они существуют в arguments.

Почему? Посмотрите раздел 15.3.4.3, где Function.prototype.apply определен. В основном вещи нам не нужны, но здесь интересная часть:

  1. Пусть len будет результатом вызова внутреннего метода [[Get]] argArray с аргументом "длина".

Что в основном означает: argArray.length. Затем спецификация выполняет простой цикл for над элементами length, создавая list соответствующих значений (list - это некоторое внутреннее voodoo, но в основном это массив). С точки зрения очень, очень свободного кода:

Function.prototype.apply = function (thisArg, argArray) {
    var len = argArray.length,
        argList = [];

    for (var i = 0; i < len; i += 1) {
        argList[i] = argArray[i];
    }

    //yeah...
    superMagicalFunctionInvocation(this, thisArg, argList);
};

Итак, все, что нам нужно для имитации argArray, в этом случае - это объект с свойством length. И теперь мы можем понять, почему значения undefined, но ключи отсутствуют, на arguments: мы создаем сопоставления key=>value.

Phew, так что это может быть не короче предыдущей части. Но когда мы закончим, там будет торт, так что будьте терпеливы! Однако, после следующего раздела (что будет коротко, я обещаю), мы можем начать рассекать выражение. В случае, если вы забыли, вопрос заключался в следующем:

Array.apply(null, { length: 5 }).map(Number.call, Number);

3. Как Array обрабатывает несколько аргументов

Итак! Мы увидели, что происходит, когда вы передаете аргумент length в Array, но в выражении мы передаем несколько аргументов как аргументы (массив из 5 undefined, если быть точным). Раздел 15.4.2.1 рассказывает нам, что делать. Последний абзац - это все, что имеет для нас значение, и это было написано очень странно, но это сводится к следующему:

function Array () {
    var ret = [];
    ret.length = arguments.length;

    for (var i = 0; i < arguments.length; i += 1) {
        ret[i] = arguments[i];
    }

    return ret;
}

Array(0, 1, 2); //[0, 1, 2]
Array.apply(null, [0, 1, 2]); //[0, 1, 2]
Array.apply(null, Array(2)); //[undefined, undefined]
Array.apply(null, {length:2}); //[undefined, undefined]

Тада! Мы получаем массив из нескольких значений undefined, и мы возвращаем массив из этих значений undefined.

Первая часть выражения

Наконец, мы можем расшифровать следующее:

Array.apply(null, { length: 5 })

Мы увидели, что он возвращает массив, содержащий 5 undefined значений, с существующими ключами.

Теперь, во вторую часть выражения:

[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)

Это будет более простая, не запутанная часть, поскольку она не столько полагается на неясные хаки.

4. Как Number обрабатывает ввод

Выполнение Number(something) (раздел 15.7.1) преобразует something в число, и это все. Как это происходит, это немного запутанно, особенно в случае строк, но операция определена в разделе 9.3 в случае, если вам интересно.

5. Игры Function.prototype.call

call является братом apply, определенным в разделе 15.3.4.4. Вместо того, чтобы брать массив аргументов, он просто принимает полученные аргументы и передает их вперед.

Все становится интереснее, когда вы соединяете более одного call вместе, закручиваете странное до 11:

function log () {
    console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^  ^-----^
// this   arguments

Это довольно достойно, пока вы не поймете, что происходит. log.call - это просто функция, эквивалентная любому другому методу call, и сама по себе имеет метод call:

log.call === log.call.call; //true
log.call === Function.call; //true

А что делает call? Он принимает thisArg и кучу аргументов и вызывает его родительскую функцию. Мы можем определить его через apply (опять же, очень свободный код, не будет работать):

Function.prototype.call = function (thisArg) {
    var args = arguments.slice(1); //I wish that'd work
    return this.apply(thisArg, args);
};

Отметьте, как это происходит:

log.call.call(log, {a:4}, {a:5});
  this = log.call
  thisArg = log
  args = [{a:4}, {a:5}]

  log.call.apply(log, [{a:4}, {a:5}])

    log.call({a:4}, {a:5})
      this = log
      thisArg = {a:4}
      args = [{a:5}]

      log.apply({a:4}, [{a:5}])

Более поздняя часть или .map всего этого

Это еще не конец. Посмотрим, что произойдет, если вы предоставите функцию большинству методов массива:

function log () {
    console.log(this, arguments);
}

var arr = ['a', 'b', 'c'];
arr.forEach(log);
//window, ['a', 0, ['a', 'b', 'c']]
//window, ['b', 1, ['a', 'b', 'c']]
//window, ['c', 2, ['a', 'b', 'c']]
//^----^  ^-----------------------^
// this         arguments

Если мы не предоставляем аргумент this, он по умолчанию равен window. Обратите внимание на порядок, в котором аргументы предоставлены нашему обратному вызову, и пусть он снова начнет стирать до 11:

arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^    ^

Whoa whoa whoa... пусть немного поднимутся. Что здесь происходит? Мы можем видеть в разделе раздел 15.4.4.18, где forEach определен, происходит следующее:

var callback = log.call,
    thisArg = log;

for (var i = 0; i < arr.length; i += 1) {
    callback.call(thisArg, arr[i], i, arr);
}

Итак, мы получаем следующее:

log.call.call(log, arr[i], i, arr);
//After one `.call`, it cascades to:
log.call(arr[i], i, arr);
//Further cascading to:
log(i, arr);

Теперь мы видим, как работает .map(Number.call, Number):

Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);

Возвращает преобразование i, текущего индекса, в число.

В заключение,

Выражение

Array.apply(null, { length: 5 }).map(Number.call, Number);

Работает в двух частях:

var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2

Первая часть создает массив из 5 элементов undefined. Второй идет по этому массиву и принимает его индексы, в результате получается массив индексов элементов:

[0, 1, 2, 3, 4]

Ответ 2

Отказ от ответственности. Это очень формальное описание вышеуказанного кода - вот как я знаю, как его объяснить. Для более простого ответа - проверьте Zirak отличный ответ выше. Это более подробное описание на вашем лице и меньше "ага".


Здесь происходит несколько вещей. Пусть немного сломается.

var arr = Array.apply(null, { length: 5 }); // Create an array of 5 `undefined` values

arr.map(Number.call, Number); // Calculate and return a number based on the index passed

В первой строке конструктор массива вызывается как функция с Function.prototype.apply.

  • Значение this равно null, что не имеет значения для конструктора Array (this является тем же самым this, что и в контексте согласно 15.3.4.3.2.a.
  • Затем вызывается new Array, передаваемый объекту с свойством length, который заставляет этот объект быть массивом, как для всего, что имеет значение .apply из-за следующего предложения в .apply:
    • Пусть len будет результатом вызова внутреннего метода [[Get]] argArray с аргументом "длина".
  • Таким образом, .apply передает аргументы от 0 до .length, так как вызов [[Get]] на { length: 5 } со значениями от 0 до 4 дает undefined конструктор массива вызывается с пятью аргументами, значение которых undefined (получение незаявленного свойства объекта).
  • Конструктор массива вызывается с 0, 2 или более аргументами. Свойство length вновь построенного массива задается числом аргументов в соответствии со спецификацией и значениями для тех же значений.
  • Таким образом, var arr = Array.apply(null, { length: 5 }); создает список из пяти значений undefined.

Примечание. Обратите внимание на разницу между Array.apply(0,{length: 5}) и Array(5), первая создающая в пять раз тип примитивного значения undefined, а вторая создающая пустой массив длины 5. В частности, из-за поведения .map (8.b) и, в частности, [[HasProperty].

Итак, приведенный выше код в соответствующей спецификации такой же, как:

var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed

Теперь переходим ко второй части.

  • Array.prototype.map вызывает функцию обратного вызова (в данном случае Number.call) для каждого элемента массива и использует указанный this значение (в этом случае значение this устанавливается на `Number).
  • Второй параметр обратного вызова на карте (в данном случае Number.call) - это индекс, а первый - это значение.
  • Это означает, что Number вызывается с this как undefined (значение массива) и индекс в качестве параметра. Таким образом, это в основном то же самое, что отображение каждого undefined в его индекс массива (поскольку вызов Number выполняет преобразование типов, в этом случае от числа до номер, не изменяющий индекс).

Таким образом, приведенный выше код берет пять значений undefined и сопоставляет каждый свой индекс в массиве.

Вот почему мы получаем результат в наш код.

Ответ 3

Как вы сказали, первая часть:

var arr = Array.apply(null, { length: 5 }); 

создает массив из 5 undefined значений.

Вторая часть вызывает функцию map массива, которая принимает 2 аргумента и возвращает новый массив того же размера.

Первый аргумент, который принимает map, фактически является функцией, применяемой к каждому элементу в массиве, ожидается, что она будет функцией, которая принимает 3 аргумента и возвращает значение. Например:

function foo(a,b,c){
    ...
    return ...
}

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

  • a как значение текущего итерированного элемента
  • b как индекс текущего итерированного элемента
  • c как весь исходный массив

Второй аргумент, который принимает map, передается функции, которую вы передаете в качестве первого аргумента. Но это не было бы a, b или c в случае foo, это было бы this.

Два примера:

function bar(a,b,c){
    return this
}
var arr2 = [3,4,5]
var newArr2 = arr2.map(bar, 9);
//newArr2 is equal to [9,9,9]

function baz(a,b,c){
    return b
}
var newArr3 = arr2.map(baz,9);
//newArr3 is equal to [0,1,2]

и еще один, чтобы сделать его более ясным:

function qux(a,b,c){
    return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]

А как насчет Number.call?

Number.call - это функция, которая принимает 2 аргумента и пытается проанализировать второй аргумент на число (я не уверен, что он делает с первым аргументом).

Так как вторым аргументом, который проходит map, является индекс, значение, которое будет помещено в новый массив при этом индексе, равно индексу. Точно так же, как функция baz в приведенном выше примере. Number.call попытается проанализировать индекс - он, естественно, вернет то же значение.

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

Ответ 4

Массив - это просто объект, содержащий поле "длина" и некоторые методы (например, push). Таким образом, arr var arr = { length: 5} в основном совпадает с массивом, где поля 0..4 имеют значение по умолчанию, которое равно undefined (т.е. arr[0] === undefined дает true).
Что касается второй части, то карта, как следует из названия, отображает от одного массива к новому. Он делает это путем прохождения через исходный массив и вызова функции отображения для каждого элемента.

Все, что осталось, - убедить вас, что результатом функции отображения является индекс. Хитрость заключается в использовании метода с именем "вызов" (*), который вызывает функцию с небольшим исключением, что первый параметр задан как контекст 'this', а второй становится первым параметром (и т.д.). По совпадению, когда функция отображения вызывается, второй параметр является индексом.

И последнее, но не менее важное: метод, который вызывается, - это номер "класс", и, как известно в JS, "класс" - это просто функция, и этот (число) ожидает, что первым параметром будет значение.

(*), найденный в прототипе функции (а Number - функция).

Mashal