Я запустил следующий код в списке рассылки 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, у него нет этих ключей. И что происходит, когда вы пытаетесь получить доступ к несуществующей собственности? Вы получаете 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
.
Теперь, когда у нас есть это, посмотрим на вторую магическую вещь:
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
- это некоторое внутреннее 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);
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)
Это будет более простая, не запутанная часть, поскольку она не столько полагается на неясные хаки.
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']]
// ^ ^
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]
Отказ от ответственности. Это очень формальное описание вышеуказанного кода - вот как я знаю, как его объяснить. Для более простого ответа - проверьте 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
:
.apply
передает аргументы от 0 до .length
, так как вызов [[Get]]
на { length: 5 }
со значениями от 0 до 4 дает undefined
конструктор массива вызывается с пятью аргументами, значение которых undefined
(получение незаявленного свойства объекта).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 и сопоставляет каждый свой индекс в массиве.
Вот почему мы получаем результат в наш код.
Как вы сказали, первая часть:
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
в вашем коде, фактически не влияет на результат. Поправьте меня, если я ошибаюсь, пожалуйста.
Массив - это просто объект, содержащий поле "длина" и некоторые методы (например, push). Таким образом, arr var arr = { length: 5}
в основном совпадает с массивом, где поля 0..4 имеют значение по умолчанию, которое равно undefined (т.е. arr[0] === undefined
дает true).
Что касается второй части, то карта, как следует из названия, отображает от одного массива к новому. Он делает это путем прохождения через исходный массив и вызова функции отображения для каждого элемента.
Все, что осталось, - убедить вас, что результатом функции отображения является индекс. Хитрость заключается в использовании метода с именем "вызов" (*), который вызывает функцию с небольшим исключением, что первый параметр задан как контекст 'this', а второй становится первым параметром (и т.д.). По совпадению, когда функция отображения вызывается, второй параметр является индексом.
И последнее, но не менее важное: метод, который вызывается, - это номер "класс", и, как известно в JS, "класс" - это просто функция, и этот (число) ожидает, что первым параметром будет значение.
(*), найденный в прототипе функции (а Number - функция).
Mashal