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

В статье "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.

5 ответов

Решение

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

    Если m равен NaN, вернуть строку "NaN",

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

Почему вы видите разные результаты для {} + [] case: при использовании его в качестве аргумента функции вы заставляете оператор быть ExpressionStatement, что делает невозможным анализ {} как пустой блок, поэтому он анализируется как литерал пустого объекта.

Это скорее комментарий, чем ответ, но по какой-то причине я не могу прокомментировать ваш вопрос. Я хотел исправить твой код 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().]

Я второе решение @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?"

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

  • + а также - операторы работают только с примитивными значениями. Более конкретно +(дополнение) работает со строками или числами, и +(одинарный) и -(вычитание и одинарное) работает только с числами.
  • Все собственные функции или операторы, которые ожидают примитивное значение в качестве аргумента, сначала преобразуют этот аргумент в желаемый тип примитива. Это сделано с 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),

Чтобы поддержать то, что было поделено ранее.

Основная причина такого поведения отчасти связана со слабо типизированной природой 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", потому что любое добавление, включающее строку, всегда будет по умолчанию для конкатенации строк.

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

Другие вопросы по тегам