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

129

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

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

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

[0, 1, 2, 3, 4]

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

Бенджамин Грюнбаум
источник
2
IMO Array.apply(null, Array(30)).map(Number.call, Number)легче читать, поскольку он избегает притворяться, что простой объект является массивом.
fncomp
10
@fncomp Пожалуйста , не используйте либо на самом деле создать диапазон. Это не только медленнее, чем простой подход - это не так легко понять. Здесь сложно понять синтаксис (ну, на самом деле API, а не синтаксис), что делает этот вопрос интересным, но ужасным производственным кодом IMO.
Бенджамин Грюнбаум
Да, не предлагая никому использовать его, но подумал, что его все же легче читать по сравнению с буквальной версией объекта.
fncomp
1
Я не уверен, зачем кому-то это нужно. Время, необходимое для создания массива таким образом, можно было бы сделать немного менее привлекательным,
Эрик Годонски

Ответы:

264

Чтобы понять этот «хакерский прием», необходимо понять несколько вещей:

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

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 с аргументом length.

Который в основном означает: 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, но в выражении мы передаем несколько вещей как аргументы (если undefinedбыть точным, массив из 5 ). В разделе 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

Это вполне достойно, пока вы не поймете, что происходит. 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]
Zirak
источник
@Zirak Помогите мне разобраться в следующем ahaExclamationMark.apply(null, Array(2)); //2, true. Почему возвращается 2и trueсоответственно? Разве вы не передаете только один аргумент, т.е. Array(2)здесь?
Компьютерщик
4
@Geek Мы передаем только один аргумент apply, но этот аргумент "разбивается" на два аргумента, передаваемых функции. На первых applyпримерах это легче увидеть . Первый из них console.logпоказывает, что мы действительно получили два аргумента (два элемента массива), а второй console.logпоказывает, что массив имеет key=>valueотображение в 1-м слоте (как объясняется в 1-й части ответа).
Zirak
4
По (некоторому) запросу теперь вы можете наслаждаться аудиоверсией: dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak
1
Обратите внимание, что передача NodeList, который является объектом хоста, собственному методу, как in log.apply(null, document.getElementsByTagName('script'));, не требуется для работы и не работает в некоторых браузерах, а [].slice.call(NodeList)преобразование NodeList в массив также не будет работать в них.
RobG
2
Одно исправление: по thisумолчанию используется только Windowнестрогий режим.
ComFreek
21

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


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

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не имеет значения для конструктора массива ( thisтакое же, thisкак в контексте согласно 15.3.4.3.2.a.
  • Затем new Arrayвызывается передача объекта со lengthсвойством, что приводит к тому, что этот объект становится массивом, как и все, что имеет значение, .applyиз-за следующего предложения в .apply:
    • Пусть len будет результатом вызова внутреннего метода [[Get]] для argArray с аргументом length.
  • Таким образом , .applyпроходят аргументы от 0 до .length, так как вызова [[Get]]на { length: 5 }со значениями от 0 до 4 выходов undefinedконструктора массива вызывается с пятью аргументов , которые имеют значение undefined(получение необъявленного свойства объекта).
  • Конструктор массива вызывается с 0, 2 или более аргументами . Свойство length вновь созданного массива устанавливается равным количеству аргументов в соответствии со спецификацией, а значения равны тем же значениям.
  • Таким образом 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).
  • Второй параметр обратного вызова в карте (в данном случае Number.call) - это индекс, а первый - это значение.
  • Это означает, что Numberвызывается с thisas undefined(значением массива) и индексом в качестве параметра. Таким образом, это в основном то же самое, что сопоставление каждого undefinedс его индексом массива (поскольку при вызове Numberвыполняется преобразование типа, в данном случае от числа к числу, не изменяя индекс).

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

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

Бенджамин Грюнбаум
источник
1
Для документов: Спецификация того, как работает карта: es5.github.io/#x15.4.4.19 , в Mozilla есть образец скрипта, который работает в соответствии с этой спецификацией, на сайте developer.mozilla.org/en-US/docs/Web/JavaScript/. Ссылка /…
Патрик Эванс,
1
Но почему это работает только с Array.apply(null, { length: 2 })и не Array.apply(null, [2])которые также вызвать Arrayконструктор проходящий в 2качестве значения длины? fiddle
Андреас
@Andreas Array.apply(null,[2])похож на Array(2)который создает пустой массив длиной 2, а не массив, содержащий примитивное значение undefinedдва раза. Смотрите мое последнее изменение в примечании после первой части, дайте мне знать, достаточно ли оно ясно, а если нет, я проясню это.
Бенджамин Грюнбаум,
Я не понял, как это работает при первом запуске ... После второго чтения это становится понятным. {length: 2}подделывает массив с двумя элементами, которые Arrayконструктор вставляет во вновь созданный массив. Поскольку нет реального массива, обращающегося к отсутствующим элементам, вы получаете, undefinedкоторый затем вставляется. Хороший трюк :)
Андреас
5

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

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

Tal Z
источник
1
Number.callне является специальной функцией, которая анализирует аргументы для чисел. Это просто === Function.prototype.call. Только второй аргумент, функция, которая передается как this-значение call, является релевантным - .map(eval.call, Number), .map(String.call, Number)и .map(Function.prototype.call, Number)все они эквивалентны.
Bergi
0

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

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

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

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

Mashal

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