Как объяснить причудливое поведение JavaScript, упомянутое в докладе «Wat» для CodeMash 2012?


753

В статье «Wat» для CodeMash 2012 в основном отмечаются некоторые странные особенности Ruby и 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 в Replay NodeJS
OrangeDog

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

1
@missingno Разместил вопрос здесь , но для {} + {}.
Ионика Бизэ

Ответы:


1480

Вот список объяснений результатов, которые вы видите (и должны видеть). Ссылки, которые я использую, взяты из стандарта 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в результате:

    Если грамматика не может интерпретировать строку как расширение StringNumericLiteral , то результатом ToNumber будет NaN .

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

    В соответствии с §15.4.1.1 и §15.4.2.2 , Array(16)создается новый массив длиной 16. Чтобы получить значение аргумента для соединения, шаги № 5 и № 6 §11.6.2 показывают, что мы должны преобразовать оба операнда в номер с помощью 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 ):

    Если m равен 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 of []. Я бы порекомендовал поискать это вместо настоящего реального объяснения. 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 только в пределах блоков function () {}. Итак, хотя я не согласен с тем, что концепция неверна, я согласен с тем, что реализация в JS ( возможно ) неверна.
Джесс Телфорд

4
@JessTelford В ES6 вы можете использовать letдля объявления переменных области блока.
Ориоль

19

Я второе решение @ Ventero. Если вы хотите, вы можете более подробно узнать, как +преобразуются его операнды.

Первый шаг (§9.1): преобразовать оба операнда в примитивы (значениями примитивов являются undefined, nullлогические, числа, строки; все другие значения являются объектами, включая массивы и функции). Если операнд уже примитивен, все готово. Если нет, то это объект, 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]'
  • {} + [], Этот заслуживает большего объяснения (см. Ответ Ventero). В этом случае фигурные скобки рассматриваются не как объект, а как пустой блок, поэтому он оказывается таким же, как +[]. Унарный +работает только с числами, поэтому реализация пытается получить число из []. Сначала он пытается, 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 являются string, number, null, undefined и boolean (Symbol скоро появится в ES6). Любое другое значение является объектом (например, массивами, функциями и объектами). Процесс принуждения для преобразования объектов в примитивные значения описывается так:

  • Если примитивное значение возвращается, когда вызывается object.valueOf (), вернуть это значение, в противном случае продолжить

  • Если примитивное значение возвращается, когда вызывается object.toString (), вернуть это значение, в противном случае продолжить

  • Бросить ошибку типа

Примечание: Для значений даты порядок должен вызывать 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-оператор JavaScript является левоассоциативным, поскольку это определяет, какие выходные данные будут иметь случаи, включающие более одной + операции.

Усиливая таким образом 1 + «2» даст «12», потому что любое добавление, включающее строку, всегда будет по умолчанию для конкатенации строк.

Вы можете прочитать больше примеров в этом сообщении в блоге (отказ от ответственности, я написал это).

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.