Яке пояснення цих химерних поведінок JavaScript, згаданих у розмові «Ват» для CodeMash 2012?


753

Говорити «Ват» для CodeMash 2012 в основному вказує кілька дивних примх з Рубі і JavaScript.

Я склав JSFiddle з результатами на http://jsfiddle.net/fe479/9/ .

Поведінки, характерні для JavaScript (як я не знаю Ruby), перераховані нижче.

У JSFiddle я виявив, що деякі мої результати не відповідають результатам на відео, і я не знаю, чому. Мені все ж цікаво знати, як JavaScript обробляє роботу за кадром у кожному випадку.

Empty Array + Empty Array
[] + []
result:
<Empty String>

Мені дуже цікаво про +оператора, коли він використовується з масивами в JavaScript. Це відповідає результату відео.

Empty Array + Object
[] + {}
result:
[Object]

Це відповідає результату відео. Що тут відбувається? Чому це об’єкт. Що робить +оператор?

Object + Empty Array
{} + []
result:
[Object]

Це не відповідає відео. Відео говорить про те, що результат дорівнює 0, тоді як я отримую [Object].

Object + Object
{} + {}
result:
[Object][Object]

Це також не відповідає відео, і як виведення змінної призводить до двох об'єктів? Можливо, мій JSFiddle помиляється.

Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

Виконання ват + 1 результатів у wat1wat1wat1wat1...

Я підозрюю, що це просто відверта поведінка, що намагання відняти число з рядка призводить до NaN.


4
{} + [], В основному, є єдиним складним і залежним від реалізації, як я пояснюю тут , оскільки це залежить від розбору як висловлювання або як виразу. У якому середовищі ви тестуєте (я отримав очікуваний 0 у Firefow та Chrome, але отримав "[об'єкт]" у NodeJs)?
hugomg

1
Я запускаю Firefox 9.0.1 на Windows 7, і JSFiddle оцінює його на [Object]
NibblyPig

@missingno Я отримую 0 у відповіді NodeJS
OrangeDog

41
Array(16).join("wat" - 1) + " Batman!"
Нік Джонсон

1
@missingno Опублікував питання тут , але для {} + {}.
Ionică Bizău

Відповіді:


1479

Ось список пояснень результатів, які ви бачите (і, як передбачається, бачите). Я використовую посилання зі стандарту ECMA-262 .

  1. [] + []

    При використанні оператора додавання і лівий, і правий операнди спочатку перетворюються на примітиви ( §11.6.1 ). Згідно з пунктом 9.1 , перетворення об'єкта (у даному випадку масиву) у примітив повертає його значення за замовчуванням, яке для об’єктів з дійсним toString()методом є результатом виклику object.toString()( §8.12.8 ). Для масивів це те саме, що викликати array.join()( §15.4.4.2 ). Приєднання порожнього масиву призводить до появи порожнього рядка, тому крок №7 оператора додавання повертає конкатенацію двох порожніх рядків, що є порожньою рядком.

  2. [] + {}

    Як і [] + []обидва операнди, перетворюються спочатку на примітиви. Для "Об'єктних об'єктів" (§15.2) це знову-таки результат виклику object.toString(), який для ненульових, невизначених об'єктів є "[object Object]"( §15.2.4.2 ).

  3. {} + []

    {}Тут не обробляються як об'єкт, а як порожній блок ( §12.1 , по крайней мере , до тих пір , поки ви не нав'язуєте цю заяву , щоб бути вираз, але про це пізніше). Повернене значення порожніх блоків порожнє, тому результат цього твердження такий же, як +[]. Унарний +оператор ( §11.4.6 ) повертає ToNumber(ToPrimitive(operand)). Як ми вже знаємо, ToPrimitive([])це порожній рядок, а відповідно до § 9.3.1 - ToNumber("")це 0.

  4. {} + {}

    Як і в попередньому випадку, перший {}розбирається як блок з порожнім значенням повернення. Знову ж, +{}те саме ToNumber(ToPrimitive({})), що і ToPrimitive({})є "[object Object]"(див. [] + {}). Отже, щоб отримати результат +{}, нам потрібно застосувати ToNumberрядок "[object Object]". Дотримуючись кроків з § 9.3.1 , отримуємо NaNв результаті:

    Якщо граматика не може інтерпретувати String як розширення StringNumericLiteral , то результатом ToNumber є NaN .

  5. Array(16).join("wat" - 1)

    Відповідно до §15.4.1.1 та §15.4.2.2 , Array(16)створює новий масив довжиною 16. Щоб отримати значення аргументу для приєднання, §11.6.2 кроки №5 та №6 показують, що ми повинні перетворити обидва операнди в кількість за допомогою ToNumber. ToNumber(1)просто 1 ( § 9.3 ), тоді як ToNumber("wat")знову це NaNвідповідно до § 9.3.1 . Дотримуючись кроку 7 §11.6.2 , §11.6.3 диктує це

    Якщо будь-який операнд - NaN , результат - NaN .

    Тож аргумент Array(16).joinє NaN. Дотримуючись §15.4.4.5 ( Array.prototype.join), ми повинні задіятиToString аргумент, який є "NaN"( § 9.8.1 ):

    Якщо т є NaN , повертає рядок "NaN".

    Дотримуючись кроку 10 §15.4.4.5 , ми отримуємо 15 повторень конкатенації "NaN"та порожнього рядка, що дорівнює результату, який ви бачите. Коли використовується "wat" + 1замість "wat" - 1аргументу, оператор додавання перетворює 1на рядок замість перетворення "wat"в число, тому він ефективно викликає Array(16).join("wat1").

Щодо того, чому ви бачите різні результати для цього {} + []випадку: Використовуючи його як аргумент функції, ви змушуєте оператор ExpressionStatement , що унеможливлює розбір {}порожнього блоку, тому він замість цього аналізується як порожній об'єкт буквальний.


2
То чому ж [] +1 => "1" і [] -1 => -1?
Роб Ельснер

4
@RobElsner []+1в значній мірі дотримується тієї ж логіки, що і []+[], 1.toString()як і rhs операнд. Для []-1бачити пояснення "wat"-1в пункті 5. Пам'ятайте , що ToNumber(ToPrimitive([]))є 0 (точка 3).
Вентеро

4
Це пояснення бракує / не містить багато деталей. Наприклад, "перетворення об'єкта (в даному випадку масиву) в примітив повертає його значення за замовчуванням, що для об'єктів з дійсним методом toString () є результатом виклику object.toString ()" повністю відсутнє, що valueOf of [] є називається спочатку, але оскільки значення, що повертається, не примітивне (це масив), замість цього використовується toString з []. Я б рекомендував шукати це замість реального пояснення пояснення 2ality.com/2012/01/object-plus-object.html
jahav

30

Це скоріше коментар, ніж відповідь, але я чомусь не можу коментувати ваше запитання. Я хотів виправити ваш код JSFiddle. Однак я опублікував це на Hacker News, і хтось запропонував, щоб я його переклав тут.

Проблема в коді JSFiddle полягає в тому, що ({})(відкриття дужок всередині дужок) не те саме, що {}(відкриття дужок як початок рядка коду). Отже, коли ви вводите, out({} + [])ви змушуєте {}те, що це не те, що це не під час введення {} + []. Це є частиною загальної 'ватності Javascript.

Основна ідея полягала в тому, що JavaScript хотів дозволити обидві ці форми:

if (u)
    v;

if (x) {
    y;
    z;
}

Для цього було зроблено дві інтерпретації вступної дужки: 1. вона не потрібна і 2. вона може з’являтися де завгодно .

Це був неправильний хід. У реальному коді немає вступного дужка, що з’являється посеред нізвідки, а реальний код також має тенденцію бути більш крихким, коли він використовує першу форму, а не другу. (Близько одного разу на другий місяць на моїй останній роботі мене телефонували до столу колеги, коли їх зміни до мого коду не працювали. Проблема полягала в тому, що вони додавали рядок до "якщо", не додаючи фігурних Я врешті-решт прийняв звичку, що фігурні брекети завжди потрібні, навіть коли ви пишете лише один рядок.)

На щастя, у багатьох випадках eval () буде повторювати повну потужність JavaScript. Код JSFiddle повинен читати:

function out(code) {
    function format(x) {
        return typeof x === "string" ?
            JSON.stringify(x) : x;
    }   
    document.writeln('&gt;&gt;&gt; ' + code);
    document.writeln(format(eval(code)));
}
document.writeln("<pre>");
out('[] + []');
out('[] + {}');
out('{} + []');
out('{} + {}');
out('Array(16).join("wat" + 1)');
out('Array(16).join("wat - 1")');
out('Array(16).join("wat" - 1) + " Batman!"');
document.writeln("</pre>");

[Крім того, це перший раз, коли я написав document.writeln за багато-багато років, і я відчуваю себе трохи брудно писати все, що стосується і document.writeln (), і eval ().]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- Я НЕ згоден (на кшталт): У мене часто в минулому використовуваних блоків , як це розмах змінних в C . Ця звичка була взята на деякий час назад, роблячи вбудований C, де змінні в стеку займають простір, тому, якщо вони більше не потрібні, ми хочемо, щоб простір було звільнено в кінці блоку. Однак ECMAScript охоплює лише область функціональних () {} блоків. Отже, хоча я не згоден з тим, що концепція неправильна, я погоджуюся, що реалізація в JS є ( можливо ) неправильною.
Джесс Телфорд,

4
@JessTelford У ES6 ви можете використовувати letдля оголошення змінних змінних.
Оріол

19

Я друге рішення @ Вентеро. Якщо ви хочете, ви можете детальніше розглянути, як +перетворює його операнди.

Перший крок (§9.1): перетворити обидва операнда до примітивів (примітивні значення undefined, null, Booleans, число, рядки, всі інші значення є об'єктами, в тому числі масивів і функцій). Якщо операнд вже примітивний, ви закінчили. Якщо ні, то це об'єкт objі виконуються наступні дії:

  1. Дзвінок obj.valueOf(). Якщо він поверне примітив, ви закінчите. Прямі екземпляри Objectта масиви повертаються самі, тому ви ще не закінчили.
  2. Дзвінок obj.toString(). Якщо він поверне примітив, ви закінчите. {}і []обидва повертають рядок, так що ви закінчили.
  3. Інакше киньте а TypeError.

Для дат заміняються крок 1 і 2. Ви можете спостерігати за поведінкою конверсії таким чином:

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

Взаємодія ( Number()спочатку перетворюється на примітивне, а потім на число):

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

Другий крок (§11.6.1): Якщо один з операндів є рядком, інший операнд також перетворюється на рядок і результат утворюється шляхом об'єднання двох рядків. В іншому випадку обидва операнди перетворюються на числа, а результат створюється шляхом їх додавання.

Більш детальне пояснення процесу перетворення: " Що таке {} + {} у JavaScript? "


13

Ми можемо посилатися на специфікацію, і це чудово і найточніше, але більшість випадків також можна пояснити більш зрозумілим способом наступними твердженнями:

  • +і -оператори працюють лише з примітивними значеннями. Більш конкретно +(додавання) працює або з рядками, або з числами, а +(унар) і -(віднімання і одинарність) працює тільки з числами.
  • Усі нативні функції або оператори, які очікують примітивного значення в якості аргументу, спочатку перетворять цей аргумент у потрібний примітивний тип. Це робиться за допомогою valueOfабо toString, які доступні на будь-якому об’єкті. Ось чому такі функції або оператори не видають помилок при виклику на об'єкти.

Тож ми можемо сказати, що:

  • [] + []те саме, String([]) + String([])що і те саме '' + ''. Я згадував вище, що +(додавання) також дійсне для чисел, але немає дійсного представлення числа масиву в JavaScript, тому замість цього використовується додавання рядків.
  • [] + {}те саме, String([]) + String({})що і те саме'' + '[object Object]'
  • {} + []. Це заслуговує більшого пояснення (див. Відповідь Вентеро). У цьому випадку фігурні дужки трактуються не як об'єкт, а як порожній блок, тому воно виявляється таким же, як +[]. Unary +працює лише з числами, тому реалізація намагається вийти з числа []. Спочатку він намагається, valueOfякий у випадку масивів повертає один і той же об'єкт, а потім намагається в останню чергу: перетворення toStringрезультату в число. Ми можемо записати це як те, +Number(String([]))що те саме, +Number('')що і те саме +0.
  • Array(16).join("wat" - 1)віднімання -працює лише з числами, так що це те саме, що:, Array(16).join(Number("wat") - 1)тому що "wat"не можна перетворити на дійсне число. Ми отримуємо NaN, і будь-яку арифметичну операцію по NaNрезультатам з NaN, так що ми маємо: Array(16).join(NaN).

0

Підкреслити те, що було поділено раніше.

Основна причина такої поведінки частково пояснюється слабко типованою природою JavaScript. Наприклад, вираз 1 + «2» неоднозначний, оскільки існує дві можливі інтерпретації на основі типів операндів (int, string) та (int int):

  • Користувач має намір об'єднати два рядки, результат: "12"
  • Користувач має намір додати два числа, результат: 3

Таким чином, при різних типах введення, можливості виходу збільшуються.

Алгоритм додавання

  1. Примусові операнди до примітивних значень

Примітиви JavaScript є рядковими, числовими, нульовими, невизначеними та булевими (символ незабаром у ES6). Будь-яке інше значення є об'єктом (наприклад, масиви, функції та об'єкти). Процес примусу для перетворення об'єктів у примітивні значення описаний таким чином:

  • Якщо примітивне значення повертається при виклику object.valueOf (), повертайте це значення, інакше продовжуйте

  • Якщо примітивне значення повертається при виклику object.toString (), повертайте це значення, інакше продовжуйте

  • Киньте TypeError

Примітка. Для значень дати вказується порядок виклику toString перед valueOf.

  1. Якщо будь-яке значення операнда є рядком, то зробіть об'єднання рядків

  2. В іншому випадку перетворіть обидва операнди в їх числове значення та додайте ці значення

Знання різних значень примусу типів у JavaScript допомагає зробити заплутані результати зрозумілішими. Дивіться таблицю примусу нижче

+-----------------+-------------------+---------------+
| Primitive Value |   String value    | Numeric value |
+-----------------+-------------------+---------------+
| null            | null            | 0             |
| undefined       | undefined       | NaN           |
| true            | true            | 1             |
| false           | false           | 0             |
| 123             | 123             | 123           |
| []              | “”                | 0             |
| {}              | “[object Object]” | NaN           |
+-----------------+-------------------+---------------+

Також добре знати, що оператор JavaScript + ліворуч асоціативний, оскільки це визначає, яким результатом будуть випадки, пов’язані з більш ніж однією операцією +.

Використовуючи таким чином 1 + "2" дасть "12", тому що будь-яке додавання, що включає рядок, завжди буде за замовчуванням для об'єднання рядків.

Ви можете прочитати більше прикладів у цій публікації в блозі (відмова я написав).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.