Я столкнулся со следующим кодом в списке рассылки es-discuss:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Это производит
[0, 1, 2, 3, 4]
Почему это результат кода? Что тут происходит?
Я столкнулся со следующим кодом в списке рассылки es-discuss:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Это производит
[0, 1, 2, 3, 4]
Почему это результат кода? Что тут происходит?
Понимание этого «хака» требует понимания нескольких вещей:
Array(5).map(...)
Function.prototype.apply
обрабатывает аргументыArray
обрабатывает несколько аргументовNumber
обрабатывает аргументыFunction.prototype.call
Это довольно сложные темы по javascript, так что это будет более чем довольно долго. Мы начнем сверху. Пристегнитесь!
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
.
Теперь, когда мы разобрались с этим, давайте посмотрим на вторую магическую вещь:
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
. В основном вещи, которые нас не волнуют, но вот интересная часть:
- Пусть 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);
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)
Это будет более простая, незамысловатая часть, так как она не так сильно зависит от непонятных хаков.
Number
обрабатывает вводВыполнение Number(something)
(раздел 15.7.1) преобразует something
в число, и это все. . Как это делается, немного запутанно, особенно в случае строк, но операция определена в разделе 9.3. если вам это интересно.
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]
ahaExclamationMark.apply(null, Array(2)); //2, true
. Почему он возвращает 2
и true
соответственно? Разве вы не передаете только один аргумент, то есть Array(2)
здесь?
- person Geek; 25.09.2013
apply
, но этот аргумент делится на два аргумента, передаваемых функции. Вы можете легко увидеть это в первых apply
примерах. Затем первый console.log
показывает, что мы действительно получили два аргумента (два элемента массива), а второй console.log
показывает, что массив имеет отображение key=>value
в 1-м слоте (как объяснено в 1-й части ответа).
- person Zirak; 25.09.2013
log.apply(null, document.getElementsByTagName('script'));
, не требуется для работы и не работает в некоторых браузерах, а [].slice.call(NodeList)
для превращения NodeList в массив также не будет работать в них.
- person RobG; 29.10.2013
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
isArrayLike = typeof obj.length == 'number' && /^[0-9]+$/.test(obj.length)
кажется достаточным. И в любом случае итерация с использованием стандартного цикла for от 0
до obj.length
почти всегда подходит, предсказуема и безошибочна (хотя, конечно, крайне скучна).
- person RobG; 30.10.2013
length - 1
был элемент. Кажется неразумным ожидать, что там будет член, когда нет теста для любого другого члена, например. {length:2.5,'1.5':'bar'}
и {length:30,'29':'bar'}
возвращают true.
- person RobG; 30.10.2013
Array.apply(null, { length: [a pretty large number] })
приведет к ошибке диапазона (превышен максимальный размер стека вызовов)
- person KooiInc; 03.07.2014
apply
, например. Function.apply(null, { length : ... })
. Вы пытаетесь запихнуть в стек много аргументов, стек недостаточно велик, RangeError
. Реквизит @copy за то, что помог мне разобраться.
- person Zirak; 04.07.2014
this
по умолчанию имеет значение Window
только в нестрогом режиме.
- person ComFreek; 07.01.2015
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.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
:
.apply
передает аргументы от 0 до .length
, поскольку вызов [[Get]]
для { length: 5 }
со значениями от 0 до 4 дает undefined
конструктор массива вызывается с пятью аргументами, значение которых равно undefined
(получение необъявленного свойства объекта).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
выполняет преобразование типа , в данном случае от номера к номеру без изменения индекса).Таким образом, приведенный выше код принимает пять неопределенных значений и сопоставляет каждое с его индексом в массиве.
Вот почему мы получаем результат в наш код.
Array.apply(null, { length: 2 })
, а не с Array.apply(null, [2])
, который также вызывает конструктор Array
, передавая 2
в качестве значения длины? скрипка
- person Andreas; 22.09.2013
Array.apply(null,[2])
подобен Array(2)
, который создает пустой массив длины 2 и не массив, содержащий примитивное значение undefined
два раза. См. мое последнее редактирование в примечании после первой части, дайте мне знать, достаточно ли оно ясно, а если нет, я уточню это.
- person Benjamin Gruenbaum; 23.09.2013
{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 в качестве первого аргумента, она будет вызываться для каждого элемента с
Второй аргумент, который принимает 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
в вашем коде, на самом деле не влияет на результат. Поправьте меня, если я ошибаюсь, пожалуйста.
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 — это функция).
МАШАЛ
[undefined, undefined, undefined, …]
и new Array(n)
или {length: n}
есть огромная разница - последние разрежены, т.е. в них нет элементов. Это очень важно для map
, поэтому использовалось нечетное Array.apply
.
- person Bergi; 23.09.2013
Array.apply(null, Array(30)).map(Number.call, Number)
легче читать, поскольку он не делает вид, что простой объект является массивом. - person fncomp   schedule 25.09.2013