Я зіткнувся з наступним кодом у списку розсилки es-дискусія:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Це виробляє
[0, 1, 2, 3, 4]
Чому це результат коду? Що тут відбувається?
Я зіткнувся з наступним кодом у списку розсилки es-дискусія:
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 з аргументом "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
, але в виразі ми передаємо декілька речей як аргументи (а саме масив із 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)
тут?
apply
, але цей аргумент "розрізаний" на два аргументи, передані функції. Це можна побачити простіше на перших apply
прикладах. Перший console.log
потім показує, що дійсно ми отримали два аргументи (два елементи масиву), а другий console.log
показує, що масив має key=>value
відображення в 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
поведінку 's (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
) - індекс, а перший - це значення.Number
викликається разом this
із 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
приймає, передається функції, яку ви передаєте як перший аргумент. Але це не було б а, 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
-Value до call
, має відношення - .map(eval.call, Number)
, .map(String.call, Number)
і .map(Function.prototype.call, Number)
все еквівалентні.
Масив - це просто об'єкт, що містить поле 'length' та деякі методи (наприклад, push). Отже, arr in 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
був використаний.
Array.apply(null, Array(30)).map(Number.call, Number)
легше читати, оскільки він уникає прикидання, що звичайний об’єкт - це масив.