Ответ 1
Понимание этого "взлома" требует понимания нескольких вещей:
- Почему мы не просто делаем
Array(5).map(...)
- Как
Function.prototype.apply
обрабатывает аргументы - Как
Array
обрабатывает несколько аргументов - Как функция
Number
обрабатывает аргументы - Что
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, у него нет этих ключей. И что происходит, когда вы пытаетесь получить доступ к несуществующей собственности? Вы получаете 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
.
Теперь, когда у нас есть это, посмотрим на вторую магическую вещь:
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
определен. В основном вещи нам не нужны, но здесь интересная часть:
- Пусть 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);
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]
Тада! Мы получаем массив из нескольких значений undefined, и мы возвращаем массив из этих значений undefined.
Первая часть выражения
Наконец, мы можем расшифровать следующее:
Array.apply(null, { length: 5 })
Мы увидели, что он возвращает массив, содержащий 5 undefined значений, с существующими ключами.
Теперь, во вторую часть выражения:
[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']]
// ^ ^
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]