Каково объяснение этих странных действий 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.

Ответ 1

Здесь приведен список объяснений результатов, которые вы видите (и должны их видеть). Ссылки, которые я использую, из стандарта ECMA-262.

  • [] + []

    При использовании оператора сложения оба левого и правого операндов сначала преобразуются в примитивы (§11.6.1). Согласно §9.1, преобразование объекта (в данном случае массива) в примитив возвращает значение по умолчанию, которое для объектов с допустимым методом toString() является результатом вызова object.toString() (§8.12.8). Для массивов это то же самое, что и вызов array.join() (§15.4.4.2). Объединение пустого массива приводит к пустой строке, поэтому шаг # 7 оператора сложения возвращает конкатенацию двух пустых строк, которая является пустой строкой.

  • [] + {}

    Подобно [] + [], оба операнда сначала преобразуются в примитивы. Для "Объектных объектов" (§15.2) это снова является результатом вызова object.toString(), который для непустых объектов не undefined имеет значение "[object Object]" (§15.2.4.2).

  • {} + []

    {} здесь не анализируется как объект, а вместо этого как пустой блок (§12.1, по крайней мере, пока вы не заставляете это выражение будет выражением, но об этом позже). Возвращаемое значение пустых блоков пуст, поэтому результат этого оператора совпадает с +[]. Унарный оператор + (§11.4.6) возвращает ToNumber(ToPrimitive(operand)). Как мы уже знаем, ToPrimitive([]) - пустая строка, и согласно §9.3.1, ToNumber("") равно 0.

  • {} + {}

    Как и в предыдущем случае, первый {} анализируется как блок с пустым возвращаемым значением. Опять же, +{} совпадает с ToNumber(ToPrimitive({})), а ToPrimitive({}) - "[object Object]" (см. [] + {}). Итак, чтобы получить результат +{}, мы должны применить ToNumber к строке "[object Object]". Следуя шагам из §9.3.1, мы получаем NaN в результате:

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

  • 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").

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

Ответ 2

Это скорее комментарий, чем ответ, но по какой-то причине я не могу комментировать ваш вопрос. Я хотел исправить код JSFiddle. Тем не менее, я разместил это в Hacker News, и кто-то предложил мне перепечатать его здесь.

Проблема в коде JSFiddle заключается в том, что ({}) (открытие скобок внутри круглых скобок) не совпадает с {} (открытие скобок как начало строки кода). Поэтому, когда вы набираете out({} + []), вы вынуждаете {} быть тем, чего нет при вводе {} + []. Это часть общей "ваттности" Javascript.

Основная идея заключалась в простом JavaScript, который хотел разрешить обе эти формы:

if (u)
    v;

if (x) {
    y;
    z;
}

Для этого были сделаны две интерпретации открывающей скобки: 1. она не требуется и 2. она может появиться где угодно.

Это был неправильный ход. Реальный код не имеет открывающей скобки, появляющейся в середине нигде, и реальный код также имеет тенденцию быть более хрупким, когда он использует первую форму, а не вторую. (Примерно раз в другой месяц на моей последней работе меня вызывали на рабочий стол, когда их изменения в моем коде не работали, и проблема заключалась в том, что они добавили строку в "if" без добавления фигурных скобки. В конце концов я просто принял привычку, что всегда нужны фигурные скобки, даже когда вы пишете только одну строку.)

К счастью, во многих случаях 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().]

Ответ 3

I второе решение @Venteros. Если вы хотите, вы можете подробнее рассказать о том, как + преобразует свои операнды.

Первый шаг (§9.1): преобразует оба операнда в примитивы (примитивные значения undefined, null, booleans, numbers, string; все остальные значения являются объектами, включая массивы и функции). Если операнд уже примитивен, все готово. Если нет, это объект obj и выполняются следующие шаги:

  • Вызов obj.valueOf(). Если он возвращает примитив, вы закончите. Прямые экземпляры Object и массивы возвращаются, поэтому вы еще не закончили.
  • Вызов obj.toString(). Если он возвращает примитив, вы закончите. {} и [] оба возвращают строку, так что вы закончили.
  • В противном случае бросьте 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?"

Ответ 4

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

  • Операторы
  • + и - работают только с примитивными значениями. Более конкретно + (дополнение) работает либо с строками, либо с цифрами, а + (унарный) и - (вычитание и унарный) работает только с числами.
  • Все собственные функции или операторы, которые ожидают примитивного значения в качестве аргумента, сначала преобразуют этот аргумент в нужный примитивный тип. Это делается с помощью valueOf или toString, доступных на любом объекте. Это причина, по которой такие функции или операторы не вызывают ошибок при вызове на объекты.

Итак, мы можем сказать, что:

  • [] + [] совпадает с String([]) + String([]), который совпадает с '' + ''. Я упомянул выше, что + (дополнение) также справедливо для чисел, но в JavaScript нет допустимого числа в массиве, поэтому вместо этого используется добавление строк.
  • [] + {} совпадает с String([]) + String({}), который совпадает с '' + '[object Object]'
  • {} + []. Это заслуживает большего объяснения (см. Ответ Вентеро). В этом случае фигурные скобки обрабатываются не как объект, а как пустой блок, поэтому он оказывается таким же, как +[]. Унарный + работает только с числами, поэтому реализация пытается получить номер из []. Сначала он пытается 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).

Ответ 5

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

Основная причина этого поведения частично связана с слабо типизированным характером JavaScript. Например, выражение 1 + "2" неоднозначно, поскольку существуют две возможные интерпретации, основанные на типах операндов (int, string) и (int int):

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

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

Алгоритм сложения

  • Использовать операнды для примитивных значений

Примитивы JavaScript - это строка, число, null, undefined и boolean (Symbol скоро появится в 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", потому что любое добавление, включающее строку, всегда будет по умолчанию конкатенацией строк.

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