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

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

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

Это производит

[0, 1, 2, 3, 4]

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


person Benjamin Gruenbaum    schedule 22.09.2013    source источник
comment
IMO Array.apply(null, Array(30)).map(Number.call, Number) легче читать, поскольку он не делает вид, что простой объект является массивом.   -  person fncomp    schedule 25.09.2013
comment
@fncomp Пожалуйста, не используйте их для на самом деле создания диапазона. Он не только медленнее прямого подхода, но и не так прост для понимания. Здесь трудно понять синтаксис (ну, на самом деле API, а не синтаксис), что делает этот вопрос интересным, но ужасным производственным кодом IMO.   -  person Benjamin Gruenbaum    schedule 25.09.2013
comment
Да, не предполагая, что кто-либо использует его, но подумал, что его все же легче читать по сравнению с литеральной версией объекта.   -  person fncomp    schedule 25.09.2013
comment
Я не уверен, почему кто-то захочет это сделать. Количество времени, необходимое для создания массива таким образом, можно было бы сделать немного менее привлекательным, но гораздо более быстрым способом: jsperf.com/basic-vs-extreme   -  person Eric Hodonsky    schedule 29.10.2013


Ответы (4)


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

  1. Почему бы нам просто не сделать Array(5).map(...)
  2. Как Function.prototype.apply обрабатывает аргументы
  3. Как Array обрабатывает несколько аргументов
  4. Как функция Number обрабатывает аргументы
  5. Что делает 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.

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

//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 описывает этот процесс. Есть куча чепухи, которая нас не волнует, но если вам удастся прочитать между строк (или вы можете просто довериться мне в этом, но не делать этого), то в основном это сводится к следующему:

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 — это какое-то внутреннее вуду, но в основном это массив). С точки зрения очень, очень свободного кода:

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. И теперь мы можем понять, почему значения не определены, а ключи нет, на arguments: мы создаем сопоставления key=>value.

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

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]

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

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

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

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

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

Теперь ко второй части выражения:

[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

Это довольно достойно WTF, пока вы не поймете, что происходит. 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']]
// ^    ^

Воу-воу-воу... давайте немного отдохнем. Что тут происходит? Мы можем видеть в раздел 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 неопределенных элементов. Второй проходит по этому массиву и берет его индексы, в результате чего получается массив индексов элементов:

[0, 1, 2, 3, 4]
person Zirak    schedule 22.09.2013
comment
@Zirak Пожалуйста, помогите мне понять следующее ahaExclamationMark.apply(null, Array(2)); //2, true. Почему он возвращает 2 и true соответственно? Разве вы не передаете только один аргумент, то есть Array(2) здесь? - person Geek; 25.09.2013
comment
@Geek Мы передаем только один аргумент apply, но этот аргумент делится на два аргумента, передаваемых функции. Вы можете легко увидеть это в первых apply примерах. Затем первый console.log показывает, что мы действительно получили два аргумента (два элемента массива), а второй console.log показывает, что массив имеет отображение key=>value в 1-м слоте (как объяснено в 1-й части ответа). - person Zirak; 25.09.2013
comment
По (некоторым) запросам теперь вы можете наслаждаться аудиоверсией: dl.dropboxusercontent.com/u/24522528/SO-answer.mp3 - person Zirak; 27.09.2013
comment
Обратите внимание, что передача NodeList, который является хост-объектом, нативному методу, как в log.apply(null, document.getElementsByTagName('script'));, не требуется для работы и не работает в некоторых браузерах, а [].slice.call(NodeList) для превращения NodeList в массив также не будет работать в них. - person RobG; 29.10.2013
comment
@RobG Вот почему я воспользовался идеей jQuery для «arrayLike»: Object.prototype.isArraylike = function (obj) { var length = obj.length, type = this.type(obj); if (this.isWindow(obj)) { return false; } if (obj.nodeType === 1 && length) { return true; } return type === "array" || type !== "function" && (length === 0 || typeof length === "number" && length > 0 && (length - 1) in obj); } - person Eric Hodonsky; 29.10.2013
comment
@Relic - это кажется чрезмерным. Подобный массиву объект должен иметь только свойство length, которое является неотрицательным целым числом, поэтому isArrayLike = typeof obj.length == 'number' && /^[0-9]+$/.test(obj.length) кажется достаточным. И в любом случае итерация с использованием стандартного цикла for от 0 до obj.length почти всегда подходит, предсказуема и безошибочна (хотя, конечно, крайне скучна). - person RobG; 30.10.2013
comment
@Relic — кроме того, ваш isArrayLike не проверяет, что length является целым числом, и не требуется, чтобы в length - 1 был элемент. Кажется неразумным ожидать, что там будет член, когда нет теста для любого другого члена, например. {length:2.5,'1.5':'bar'} и {length:30,'29':'bar'} возвращают true. - person RobG; 30.10.2013
comment
Да, это может быть ассоциативным массивом или «массивом». Так что... это именно то, что я хочу... это определенно разумно, лол... Я бы хотел, чтобы они возвращали true. Плюс, как я уже сказал... команда jQuery придумала это. - person Eric Hodonsky; 31.10.2013
comment
Обратите внимание, что Array.apply(null, { length: [a pretty large number] }) приведет к ошибке диапазона (превышен максимальный размер стека вызовов) - person KooiInc; 03.07.2014
comment
@KooiInc Это верно для любого такого использования apply, например. Function.apply(null, { length : ... }). Вы пытаетесь запихнуть в стек много аргументов, стек недостаточно велик, RangeError. Реквизит @copy за то, что помог мне разобраться. - person Zirak; 04.07.2014
comment
Одно исправление: this по умолчанию имеет значение Window только в нестрогом режиме. - person ComFreek; 07.01.2015
comment
также Array.from(new Array(5)).map(fn), кажется, делает то же самое - person bitcruncher; 18.11.2017

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


Здесь происходит несколько вещей. Давайте немного разобьем.

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.
  • Then new Array is called being passed an object with a length property - that causes that object to be an array like for all it matters to .apply because of the following clause in .apply:
    • Let len be the result of calling the [[Get]] internal method of argArray with argument "length".
  • Таким образом, .apply передает аргументы от 0 до .length , поскольку вызов [[Get]] для { length: 5 } со значениями от 0 до 4 дает undefined конструктор массива вызывается с пятью аргументами, значение которых равно undefined (получение необъявленного свойства объекта).
  • Конструктор массива вызывается с 0, 2 или более аргументами. Свойство длины вновь созданного массива устанавливается равным количеству аргументов в соответствии со спецификацией, а значения равны тем же значениям.
  • Таким образом, var arr = Array.apply(null, { length: 5 }); создает список из пяти неопределенных значений.

Примечание. Обратите внимание на разницу между 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.call) — это индекс, а первый — значение this.
  • Это означает, что Number вызывается с this как undefined (значение массива) и индексом в качестве параметра. Таким образом, это в основном то же самое, что и сопоставление каждого undefined с его индексом массива (поскольку вызов Number выполняет преобразование типа , в данном случае от номера к номеру без изменения индекса).

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

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

person Benjamin Gruenbaum    schedule 22.09.2013
comment
Для документов: Спецификация работы карты: es5.github.io/#x15.4.4.19 у Mozilla есть пример сценария, который работает в соответствии с этой спецификацией, по адресу developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ - person Patrick Evans; 22.09.2013
comment
Но почему это работает только с Array.apply(null, { length: 2 }), а не с Array.apply(null, [2]), который также вызывает конструктор Array, передавая 2 в качестве значения длины? скрипка - person Andreas; 22.09.2013
comment
@Andreas Array.apply(null,[2]) подобен Array(2), который создает пустой массив длины 2 и не массив, содержащий примитивное значение undefined два раза. См. мое последнее редактирование в примечании после первой части, дайте мне знать, достаточно ли оно ясно, а если нет, я уточню это. - person Benjamin Gruenbaum; 23.09.2013
comment
Я не понял, как это работает при первом запуске... После второго чтения это становится понятным. {length: 2} имитирует массив с двумя элементами, которые конструктор Array вставит во вновь созданный массив. Поскольку нет реального массива, доступ к отсутствующим элементам дает undefined, который затем вставляется. Хороший трюк :) - person Andreas; 23.09.2013

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

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 в вашем коде, на самом деле не влияет на результат. Поправьте меня, если я ошибаюсь, пожалуйста.

person Tal Z    schedule 22.09.2013
comment
Number.call не является специальной функцией, которая преобразует аргументы в числа. Это просто === Function.prototype.call. Имеет значение только второй аргумент, функция, которая передается как значение this в call — все .map(eval.call, Number), .map(String.call, Number) и .map(Function.prototype.call, Number) эквивалентны. - person Bergi; 23.09.2013

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

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

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

(*) находится в прототипе Function (а Number — это функция).

МАШАЛ

person shex    schedule 22.09.2013
comment
Между [undefined, undefined, undefined, …] и new Array(n) или {length: n} есть огромная разница - последние разрежены, т.е. в них нет элементов. Это очень важно для map, поэтому использовалось нечетное Array.apply. - person Bergi; 23.09.2013