在CodeMash 2012的“ Wat”演讲中提到的这些怪异JavaScript行为的解释是什么?


753

CodeMash 2012“ Wat”演讲基本上指出了Ruby和JavaScript的一些怪异之处。

我在http://jsfiddle.net/fe479/9/上对结果做了JSFiddle 。

下面列出了特定于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

做wat + 1会导致wat1wat1wat1wat1...

我怀疑这是直接的行为,即试图从字符串中减去数字会导致NaN。


4
正如我在这里解释的,{} + []基本上是唯一棘手且依赖实现的,因为它依赖于被解析为语句还是表达式。您在什么环境中进行测试(我在Firefow和Chrome中获得了预期的0,但在NodeJ中获得了“ [object Object]”)?
hugomg 2012年

1
我在Windows 7上运行Firefox 9.0.1,并且JSFiddle将其评估为[对象]
NibblyPig

@missingno我在
NodeJS

41
Array(16).join("wat" - 1) + " Batman!"
尼克·约翰逊

1
@missingno在此处发布问题,但要提交{} + {}
尼卡比曹

Answers:


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.2Array(16)创建一个长度为16的新数组。要获取要加入的参数的值,§11.6.2步骤#5和#6显示我们必须将两个操作数都转换为a。使用编号ToNumberToNumber(1)仅仅是1(第9.3节),而ToNumber("wat")再次NaN§9.3.1。遵循§11.6.2的步骤7 ,§11.6.3规定:

    如果任一操作数为NaN,则结果为NaN

    因此,的论据Array(16).joinNaN。在§15.4.4.5(Array.prototype.join)之后,我们必须调用ToString参数"NaN"§9.8.1):

    如果mNaN,则返回String "NaN"

    §15.4.4.5的步骤10 之后,我们得到了15个重复的串联连接"NaN"和空字符串,这等于您看到的结果。当使用"wat" + 1代替"wat" - 1作为参数时,加法运算符将转换1为字符串而不是"wat"数字,因此它有效地调用Array(16).join("wat1")

关于为什么看到这种{} + []情况的不同结果:当将其用作函数参数时,您将强制该语句为ExpressionStatement,这使得无法将其解析{}为空块,因此将其解析为空对象文字。


2
那么,为什么[] +1 =>“ 1”和[] -1 => -1?
Rob Elsner 2014年

4
@RobElsner 与rhs操作数[]+1几乎遵循相同的逻辑。有关详细信息,请参见第5点。请记住,该值为0(第3点)。[]+[]1.toString()[]-1"wat"-1ToNumber(ToPrimitive([]))
Ventero 2014年

4
该解释缺少/省略了很多细节。例如,“将对象(在本例中为数组)转换为原语将返回其默认值,对于具有有效toString()方法的对象,这是调用object.toString()的结果”,完全缺少[]的valueOf首先调用,但是因为返回值不是原始值(它是一个数组),所以使用[]的toString代替。我建议您改用它进行真正的深入解释2ality.com/2012/01/object-plus-object.html
jahav

30

这更多是评论,而不是答案,但是由于某些原因,我无法评论您的问题。我想更正您的JSFiddle代码。但是,我将此内容发布在了《黑客新闻》上,有人建议我在此处重新发布。

JSFiddle代码中的问题是({})(括号内的大括号)与{}(括号内的代码行开头)不同。因此,当您键入内容时,out({} + [])您将强制输入{}而不是在键入内容时{} + []。这是Java整体“浪费”的一部分。

基本思想是简单的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()的任何东西我感到有点脏。]


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中的实现是(可能)是错误的。
杰西·特尔福德2013年

4
@JessTelford在ES6中,可以let用来声明块作用域变量。
Oriol

19

我第二个@Ventero的解决方案。如果需要,可以更详细地了解如何+转换其操作数。

第一步骤(第9.1节):两个操作数转换成原语(原始值undefinednull,布尔值,数字,字符串;所有其它值都是对象,包括数组和功能)。如果操作数已经是原始的,则操作完成。如果不是,则为对象,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原语是字符串,数字,空值,未定义和布尔值(Symbol在ES6中即将推出)。其他任何值都是一个对象(例如,数组,函数和对象)。因此描述了将对象转换为原始值的强制过程:

  • 如果在调用object.valueOf()时返回了原始值,则返回该值,否则继续

  • 如果在调用object.toString()时返回了原始值,则返回该值,否则继续

  • 引发TypeError

注意:对于日期值,顺序是在valueOf之前调用toString。

  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”,因为涉及字符串的任何加法将始终默认为字符串连接。

您可以在此博客文章中阅读更多示例(我写过免责声明)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.