Как объяснить причудливое поведение 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.
[] + []
При использовании оператора сложения и левый, и правый операнды сначала преобразуются в примитивы ( §11.6.1). Согласно §9.1, преобразование объекта (в данном случае массива) в примитив возвращает его значение по умолчанию, которое для объектов с допустимым
toString()
метод является результатом вызоваobject.toString()
( §8.12.8). Для массивов это то же самое, что вызовarray.join()
( §15.4.4.2). Присоединение к пустому массиву приводит к пустой строке, поэтому шаг #7 оператора сложения возвращает конкатенацию двух пустых строк, которая является пустой строкой.[] + {}
Похожий на
[] + []
оба операнда сначала преобразуются в примитивы. Для "Объектных объектов" (§15.2) это снова результат вызоваobject.toString()
, что для ненулевых, неопределенных объектов"[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
в следствии:Если грамматика не может интерпретировать String как расширение 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")
,
Почему вы видите разные результаты для {} + []
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('>>> ' + 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
и выполняются следующие шаги:
- Вызов
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?"
Мы можем ссылаться на спецификацию, и это здорово и наиболее точно, но большинство случаев можно также объяснить более понятным способом с помощью следующих утверждений:
+
а также-
операторы работают только с примитивными значениями. Более конкретно+
(дополнение) работает со строками или числами, и+
(одинарный) и-
(вычитание и одинарное) работает только с числами.- Все собственные функции или операторы, которые ожидают примитивное значение в качестве аргумента, сначала преобразуют этот аргумент в желаемый тип примитива. Это сделано с
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
Таким образом, с различными типами ввода, возможности вывода увеличиваются.
Алгоритм сложения
- Привести операнды к примитивным значениям
Примитивами JavaScript являются string, number, null, undefined и boolean (Symbol скоро появится в ES6). Любое другое значение является объектом (например, массивами, функциями и объектами). Процесс принуждения для преобразования объектов в примитивные значения описывается так:
Если примитивное значение возвращается при вызове object.valueOf (), то возвращает это значение, в противном случае продолжить
Если примитивное значение возвращается, когда вызывается object.toString (), вернуть это значение, в противном случае продолжить
Бросить ошибку типа
Примечание: Для значений даты порядок должен вызывать toString перед valueOf.
Если любое значение операнда является строкой, выполните конкатенацию строк
В противном случае преобразуйте оба операнда в их числовое значение, а затем добавьте эти значения.
Знание различных значений приведения типов в 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", потому что любое добавление, включающее строку, всегда будет по умолчанию для конкатенации строк.
Вы можете прочитать больше примеров в этом сообщении в блоге (отказ от ответственности, я написал это).