Створення діапазону в JavaScript - дивний синтаксис


129

Я зіткнувся з наступним кодом у списку розсилки es-дискусія:

Array.apply(null, { length: 5 }).map(Number.call, Number);

Це виробляє

[0, 1, 2, 3, 4]

Чому це результат коду? Що тут відбувається?


2
IMO Array.apply(null, Array(30)).map(Number.call, Number)легше читати, оскільки він уникає прикидання, що звичайний об’єкт - це масив.
fncomp

10
@fncomp ласка , не використовуйте або на самому ділі створити діапазон. Мало того, що він повільніше, ніж прямолінійний підхід, - це не майже так просто зрозуміти. Тут важко зрозуміти синтаксис (ну, справді API, а не синтаксис), що робить це цікавим питанням, але жахливим виробничим кодом IMO.
Бенджамін Грюнбаум

Так, не пропонуючи комусь скористатися, але подумав, що його все-таки легше читати відносно об’єктної буквальної версії.
fncomp

1
Я не впевнений, чому хтось хотів би це зробити. Кількість часу, необхідного для створення масиву таким чином, могло бути здійснено трохи менш сексуально, але набагато швидше: jsperf.com/basic-vs-extreme
Ерік Ходонський

Відповіді:


263

Розуміння цього "зламу" вимагає розуміння кількох речей:

  1. Чому ми не просто робимо Array(5).map(...)
  2. Як Function.prototype.applyобробляє аргументи
  3. Як Arrayобробляє кілька аргументів
  4. Як Numberфункція обробляє аргументи
  5. Що 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.

Тепер ми можемо трохи підняти голову і побачити, чому такі функції, як 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властивість.

Тепер, коли нам це не вдається, давайте подивимось на другу магічну річ:

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визначено. Переважно речі, які нас не цікавлять, але ось цікава частина:

  1. Нехай 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);

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]

Тада! Ми отримуємо масив з декількох невизначених значень і повертаємо масив цих невизначених значень.

Перша частина виразу

Нарешті, ми можемо розшифрувати наступне:

Array.apply(null, { length: 5 })

Ми бачили, що він повертає масив, що містить 5 невизначених значень, з ключами, що існують.

Тепер до другої частини виразу:

[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

Це цілком варте 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]

@ Zirak Будь ласка, допоможіть мені зрозуміти наступне ahaExclamationMark.apply(null, Array(2)); //2, true. Чому він повертається 2і trueвідповідно? Ви не передаєте лише один аргумент, тобто Array(2)тут?
Geek

4
@Geek Ми передаємо лише один аргумент apply, але цей аргумент "розрізаний" на два аргументи, передані функції. Це можна побачити простіше на перших applyприкладах. Перший console.logпотім показує, що дійсно ми отримали два аргументи (два елементи масиву), а другий console.logпоказує, що масив має key=>valueвідображення в 1-му слоті (як пояснено в першій частині відповіді).
Зірак

4
З - за (деякі) запиту , тепер ви можете насолоджуватися аудіо версію: dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak

1
Зауважте, що передача NodeList, що є об'єктом хосту, до нативного методу, як у log.apply(null, document.getElementsByTagName('script'));, не потрібна для роботи та не працює в деяких браузерах, а [].slice.call(NodeList)перетворення NodeList в масив також не буде працювати в них.
RobG

2
Одне виправлення: thisза замовчуванням застосовується лише нестрогий Windowрежим.
ComFreek

21

Відмова : Це дуже формальний опис вищевказаного коду - саме так я знаю, як це пояснити. Для більш простої відповіді - перевірте чудову відповідь Зірака вище. Це більш глибока специфікація вашого обличчя і менше "ага".


Тут відбувається кілька речей. Давайте трохи розберемо.

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:
    • Нехай len є результатом виклику [[Get]] внутрішнього методу argArray з аргументом "length".
  • Таким чином , .applyпроходять аргументи від 0 до .length, так як виклику [[Get]]на { length: 5 }зі значеннями від 0 до 4 виходів undefinedконструктора масиву викликається з п'ятьма аргументів , які мають значення undefined(отримання неоголошеного властивості об'єкта).
  • Конструктор масиву викликається 0, 2 або більше аргументів . Властивість довжини щойно побудованого масиву встановлюється на кількість аргументів відповідно до специфікації та значення на однакові значення.
  • Таким чином 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виконує перетворення типів, в цьому випадку з числа в число не змінюється індекс).

Таким чином, наведений вище код приймає п'ять неозначених значень і відображає кожне з його індексів у масиві.

Ось чому ми отримуємо результат до нашого коду.


1
Для документів: Специфікація того, як працює карта: es5.github.io/#x15.4.4.19 , Mozilla має зразок сценарію, який функціонує відповідно до цієї специфікації на developer.mozilla.org/en-US/docs/Web/JavaScript/ Довідка /…
Патрік Еванс

1
Але чому це працює тільки з, Array.apply(null, { length: 2 })а не, Array.apply(null, [2])що також називає Arrayконструктор, що передає 2як значення довжини? fiddle
Андреас

@Andreas Array.apply(null,[2])- це такий, Array(2)що створює порожній масив довжиною 2, а не масив, що містить примітивне значення undefinedдва рази. Дивіться мою останню редакцію в примітці після першої частини, дайте мені знати, чи достатньо ясно, а якщо ні, я уточню це.
Бенджамін Грюнбаум

Я не зрозумів, як це працює під час першого запуску ... Після другого читання це має сенс. {length: 2}підробляє масив з двома елементами, який Arrayконструктор вставив би в новостворений масив. Оскільки немає справжнього масиву, що звертається до присутніх елементів, виходить результат, undefinedякий потім вставляється. Хороший трюк :)
Андреас

5

Як ви вже сказали, перша частина:

var arr = Array.apply(null, { length: 5 }); 

створює масив з 5 undefinedзначень.

Друга частина викликає mapфункцію масиву, який бере 2 аргументи і повертає новий масив такого ж розміру.

Перший аргумент, який mapприймає, - це фактично функція, яка застосовується до кожного елемента масиву, очікується, що це функція, яка бере 3 аргументи і повертає значення. Наприклад:

function foo(a,b,c){
    ...
    return ...
}

якщо ми передамо функцію foo в якості першого аргументу, вона буде викликана для кожного елемента

  • a як значення поточного ітераційного елемента
  • b як індекс поточного ітераційного елемента
  • c як весь вихідний масив

Другий аргумент, який 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функції в коді, насправді не впливає на результат. Виправте мене, якщо я помиляюся, будь ласка.


1
Number.callнемає спеціальної функції, яка б розбирала аргументи на числа. Це просто === Function.prototype.call. Тільки другий аргумент, функція , яка отримує передається як this-Value до call, має відношення - .map(eval.call, Number), .map(String.call, Number)і .map(Function.prototype.call, Number)все еквівалентні.
Бергі

0

Масив - це просто об'єкт, що містить поле 'length' та деякі методи (наприклад, push). Отже, arr in var arr = { length: 5}- це в основному такий же, як масив, де поля 0..4 мають значення за замовчуванням, яке не визначене (тобто значення arr[0] === undefinedtrue).
Що стосується другої частини, карта, як випливає з назви, відображає карти з одного масиву на новий. Це робиться шляхом переходу через оригінальний масив та виклику функції відображення кожного елемента.

Залишилося лише переконати вас, що результатом функції відображення є індекс. Трюк полягає у використанні методу з назвою "call" (*), який викликає функцію за малим винятком, що перший параметр встановлений як контекст "цього", а другий стає першим парам (і так далі). Випадково, коли викликається функція відображення, другим парам є індекс.

І останнє, але не менш важливе, метод, на який посилається, - це число «Клас», і, як ми знаємо в JS, «Клас» - це просто функція, і цей (Число) очікує, що перший параметр буде значенням.

(*), знайдений в прототипі Function (а Number - функція).

МАШАЛ


1
Існує величезна різниця між [undefined, undefined, undefined, …]і new Array(n)або {length: n}- останні є рідкісними , тобто в них немає елементів. Це дуже актуально для map, і тому незвичайний Array.applyбув використаний.
Бергі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.