Я зіткнувся з наступним кодом у списку розсилки 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.callcallє 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] === undefinedtrue).
Що стосується другої частини, карта, як випливає з назви, відображає карти з одного масиву на новий. Це робиться шляхом переходу через оригінальний масив та виклику функції відображення кожного елемента.
Залишилося лише переконати вас, що результатом функції відображення є індекс. Трюк полягає у використанні методу з назвою "call" (*), який викликає функцію за малим винятком, що перший параметр встановлений як контекст "цього", а другий стає першим парам (і так далі). Випадково, коли викликається функція відображення, другим парам є індекс.
І останнє, але не менш важливе, метод, на який посилається, - це число «Клас», і, як ми знаємо в JS, «Клас» - це просто функція, і цей (Число) очікує, що перший параметр буде значенням.
(*), знайдений в прототипі Function (а Number - функція).
МАШАЛ
[undefined, undefined, undefined, …]і new Array(n)або {length: n}- останні є рідкісними , тобто в них немає елементів. Це дуже актуально для map, і тому незвичайний Array.applyбув використаний.
Array.apply(null, Array(30)).map(Number.call, Number)легше читати, оскільки він уникає прикидання, що звичайний об’єкт - це масив.