Я наткнулся на следующий код в списке рассылки es-Disc:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Это производит
[0, 1, 2, 3, 4]
Почему это результат кода? Что тут происходит?
Я наткнулся на следующий код в списке рассылки es-Disc:
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]
был просто 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
свойство.
Теперь, когда у нас это есть, давайте посмотрим на вторую волшебную вещь:
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 с аргументом 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);
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)
Это будет более легкая и понятная часть, поскольку она не так сильно полагается на малоизвестные хаки.
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
Это вполне достойно, пока вы не поймете, что происходит. 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)
здесь?
apply
, но этот аргумент "разбивается" на два аргумента, передаваемых функции. На первых apply
примерах это легче увидеть . Первый из них console.log
показывает, что мы действительно получили два аргумента (два элемента массива), а второй console.log
показывает, что массив имеет key=>value
отображение в 1-м слоте (как объясняется в 1-й части ответа).
log.apply(null, document.getElementsByTagName('script'));
, не требуется для работы и не работает в некоторых браузерах, а [].slice.call(NodeList)
преобразование NodeList в массив также не будет работать в них.
this
умолчанию используется только Window
нестрогий режим.
Отказ от ответственности : это очень формальное описание приведенного выше кода - вот как я знаю, как его объяснить. Для более простого ответа - посмотрите отличный ответ Зирака выше. Это более подробное описание вашего лица и меньше «ага».
Здесь происходит несколько вещей. Давайте немного разберемся.
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
:
.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).Number.call
) - это индекс, а первый - это значение.Number
вызывается с this
as undefined
(значением массива) и индексом в качестве параметра. Таким образом, это в основном то же самое, что сопоставление каждого undefined
с его индексом массива (поскольку при вызове Number
выполняется преобразование типа, в данном случае от числа к числу, не изменяя индекс).Таким образом, приведенный выше код принимает пять неопределенных значений и сопоставляет каждое со своим индексом в массиве.
Вот почему мы получаем результат в нашем коде.
Array.apply(null,[2])
похож на Array(2)
который создает пустой массив длиной 2, а не массив, содержащий примитивное значение undefined
два раза. Смотрите мое последнее изменение в примечании после первой части, дайте мне знать, достаточно ли оно ясно, а если нет, я проясню это.
{length: 2}
подделывает массив с двумя элементами, которые Array
конструктор вставляет во вновь созданный массив. Поскольку нет реального массива, обращающегося к отсутствующим элементам, вы получаете, undefined
который затем вставляется. Хороший трюк :)
Как вы сказали, первая часть:
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)
все они эквивалентны.
Массив - это просто объект, содержащий поле «длина» и некоторые методы (например, push). Таким образом, arr in var arr = { length: 5}
в основном совпадает с массивом, в котором поля 0..4 имеют значение по умолчанию, которое не определено (т.е. arr[0] === undefined
дает true).
Что касается второй части, map, как следует из названия, отображает один массив в новый. Это достигается путем обхода исходного массива и вызова функции сопоставления для каждого элемента.
Все, что осталось, - это убедить вас, что результатом функции сопоставления является индекс. Уловка состоит в том, чтобы использовать метод с именем 'call' (*), который вызывает функцию с небольшим исключением: первый параметр установлен как контекст 'this', а второй становится первым параметром (и так далее). По совпадению, когда вызывается функция отображения, второй параметр - это индекс.
И последнее, но не менее важное: вызываемый метод - это Number «Class», и, как мы знаем в JS, «Class» - это просто функция, и этот метод (Number) ожидает, что первый параметр будет значением.
(*) находится в прототипе функции (а Number - это функция).
Mashal
[undefined, undefined, undefined, …]
и new Array(n)
или {length: n}
- последние редки , то есть в них нет элементов. Это очень актуально для map
, и поэтому Array.apply
было использовано нечетное число .
Array.apply(null, Array(30)).map(Number.call, Number)
легче читать, поскольку он избегает притворяться, что простой объект является массивом.