这些奇怪的JavaScript行为在CodeMash 2012年的“Wat”演讲中提到,有何解释?

781

'Wat' talk for CodeMash 2012 是一次关于 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
在 JavaScript 中,{} + [] 基本上是唯一棘手且依赖于实现的内容,如我在这里所解释的(https://dev59.com/cF_Va4cB1Zd3GeqPWLlx#9021351),因为它取决于被解析为语句还是表达式。您在哪个测试环境中进行测试(我在 Firefox 和 Chrome 中得到了预期的 0,但在 NodeJs 中得到了 "[object Object]")? - hugomg
1
我在Windows 7上运行Firefox 9.0.1,而JSFiddle将其评估为[Object]。 - NibblyPig
@missingno 我在 NodeJS REPL 中得到了 0。 - OrangeDog
在 REPL 中运行的 JavaScript 与直接从代码执行的 JavaScript 是不同的。 - EhevuTov
51
Array(16).join("wat" - 1) + " Batman!" - Nick Johnson
1
@missingno 在这里发布了问题链接,但是针对的是{} + {} - Ionică Bizău
5个回答

1516
这里是关于你看到的结果(以及应该看到的结果)的解释列表。我使用的参考资料来自ECMA-262标准
  1. [] + []

    当使用加法运算符时,左右操作数首先被转换为原始值(§11.6.1)。根据§9.1,将对象(在本例中是数组)转换为原始值返回其默认值,对于具有有效toString()方法的对象来说,这个默认值是调用object.toString()的结果(§8.12.8)。对于数组来说,这与调用array.join()(§15.4.4.2)相同。连接一个空数组会导致一个空字符串,因此加法运算符的第7步返回两个空字符串的连接,即空字符串。

  2. [] + {}

    [] + []类似,两个操作数都首先被转换为原始值。对于"Object objects"(§15.2),这又是调用object.toString()的结果,对于非null、非undefined对象来说,这个结果是"[object Object]"(§15.2.4.2)。

  3. {} + []

    这里的{}不是解析为对象,而是解析为空块(§12.1),至少在你没有强制该语句成为表达式的情况下是这样,但稍后会更详细地讨论。空块的返回值为空,因此该语句的结果与+[]相同。一元的+运算符(§11.4.6)返回ToNumber(ToPrimitive(operand))。正如我们已经知道的那样,ToPrimitive([])是空字符串,根据§9.3.1ToNumber("")是0。

  4. {} + {}

    类似于前一个案例,第一个{}被解析为具有空返回值的块。同样,+{}ToNumber(ToPrimitive({}))相同,ToPrimitive({})"[object Object]"(参见[] + {})。因此,要得到+{}的结果,我们必须对字符串"[object Object]"应用ToNumber。当按照§9.3.1的步骤进行时,我们得到NaN作为结果:

    如果语法无法将字符串解释为StringNumericLiteral的扩展,则ToNumber的结果为NaN

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

    根据§15.4.1.1§15.4.2.2Array(16)创建一个长度为16的新数组。要获取join的参数的值,§11.6.2的步骤#5和#6显示,我们必须使用ToNumber将两个操作数都转换为数字。 ToNumber(1)只是1(§9.3),而ToNumber("wat")再次是NaN(根据§9.3.1)。按照§11.6.2的第7步,{{link

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

2
为什么 []+1 的结果是 "1",而 []-1 的结果是 -1? - Rob Elsner
4
@RobElsner []+1 的逻辑与 []+[] 相同,只是将 1.toString() 作为右操作数。关于 []-1,请参见第5点中 "wat"-1 的解释。请记住,ToNumber(ToPrimitive([])) 等于0(第3点)。 - Ventero
4
这个解释缺少很多细节,例如,“将对象(在这种情况下是数组)转换为基元类型将返回其默认值。对于具有有效toString()方法的对象,其默认值是调用object.toString()的结果。” 完全没有提及首先调用[]的valueOf,但由于返回值不是基元类型(它是一个数组),因此使用了[]的toString。我建议查看这个链接,以获得真正深入的解释:http://www.2ality.com/2012/01/object-plus-object.html - jahav

33

这更像是一条评论而不是一个回答,但由于某种原因我无法在您的问题上进行评论。我想纠正你的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(),同时涉及 eval(),感觉有些不妥。]


16
这是一个错误的行为。真正的代码不会在无中生有的地方出现大括号 - 我有不同看法(有点):过去我经常在C语言中使用像这样的块来限定变量的作用域。这个习惯是很早以前做嵌入式C时养成的,因为栈上的变量占用空间,如果它们不再需要,我们希望在块的末尾释放空间。然而,在ECMAScript中,作用域仅限于函数(){}块内部。因此,虽然我不同意这个概念是错误的,但我同意JS中的实现可能是(可能)错误的。 - Jess Telford
5
在ES6中,你可以使用let声明块级作用域变量。 - Oriol

20

我支持 @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中{} + {}是什么?


14

我们可以参考规范,这是很好的且最准确的方法,但在大多数情况下以下陈述可以更加易懂:

  • +- 运算符仅适用于原始值。更具体地说,+(加法)适用于字符串或数字,+(一元运算)和 -(减法和一元运算)仅适用于数字。
  • 所有期望原始值作为参数的本地函数或运算符,都会首先将该参数转换为所需的原始类型。这是通过 valueOftoString 执行的,这两个方法在任何对象上都可用。这就是为什么这些函数或运算符在调用对象时不会抛出错误的原因。

因此我们可以这样说:

  • [] + [] 等同于 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)

  • 1
    为了加强之前分享的内容。
    这种行为的根本原因部分是由于JavaScript的弱类型特性。例如,表达式1 +“2”是模棱两可的,因为有两种基于操作数类型(int,string)和(int int)的可能解释:
    - 用户打算连接两个字符串,结果:“12” - 用户打算添加两个数字,结果:3
    因此,随着输入类型的不同,输出可能性增加。
    加法算法
    1.将操作数强制转换为原始值
    JavaScript的原始类型包括字符串、数字、null、undefined和布尔值(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",因为任何涉及字符串的加法都会默认转换为字符串连接。

    你可以在这篇博客文章中阅读更多示例(免责声明:我撰写了此文)。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接