对于CodeMash 2012的“Wat”演讲中提到的这些奇怪的JavaScript行为有什么解释?

What is the explanation for these bizarre JavaScript behaviours mentioned in the 'Wat' talk for CodeMash 2012?

codemash 2012的"wat"讨论基本上指出了Ruby和JavaScript的一些奇怪的特性。

我在http://jsfiddle.net/fe479/9/上对结果进行了jsfiddle处理。

下面列出了特定于javascript(我不知道Ruby)的行为。

我在jsFiddle中发现我的一些结果与视频中的结果不一致,我不知道为什么。不过,我很想知道在每种情况下,javascript是如何处理幕后工作的。

1
2
3
4
Empty Array + Empty Array
[] + []
result:
<Empty String>

我很好奇在与JavaScript中的数组一起使用时,+操作符。这与视频的结果相匹配。

1
2
3
4
Empty Array + Object
[] + {}
result:
[Object]

这与视频的结果相匹配。这是怎么回事?为什么这是一个物体。+操作符做什么?

1
2
3
4
Object + Empty Array
{} + []
result
[Object]

这与视频不匹配。视频显示结果是0,而我得到了[对象]。

1
2
3
4
Object + Object
{} + {}
result:
[Object][Object]

这也与视频不匹配,输出变量如何产生两个对象?也许我的琴错了。

1
2
3
Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

做wat+1会导致wat1wat1wat1wat1

我怀疑这只是简单的行为,尝试从一个字符串中减去一个数字会得到NaN。


以下是您看到的(和应该看到的)结果的解释列表。我使用的参考资料来自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]"相同(见[] + {})。因此,为了得到+{}的结果,我们必须在字符串"[object Object]"上应用ToNumber。当按照第9.3.1节中的步骤进行操作时,我们得到NaN,结果:


    If the grammar cannot interpret the String as an expansion of StringNumericLiteral, then the result of ToNumber is 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")又是根据§9.3.1的NaN。在§11.6.2第7步之后,§11.6.3规定


    If either operand is NaN, the result is NaN.

    因此,对Array(16).join的论点是NaN。根据第15.4.4.5节(Array.prototype.join的规定,我们必须在论点上称为ToString,即"NaN"号(§9.8.1):

    If m is NaN, return the String "NaN".

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

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


    这与其说是回答,不如说是评论,但出于某种原因,我不能对你的问题发表评论。我想更正您的JSfiddle代码。然而,我把这个贴在黑客新闻上,有人建议我把它转寄到这里。

    JSfiddle代码中的问题是({})(括号内的左大括号)与{}不同(将大括号作为一行代码的开头)。所以当你输入out({} + [])时,你是在强迫{}成为某种东西,而当你输入{} + []时,它不是这样。这是JavaScript整体"wat"特性的一部分。

    基本的想法是简单的javascript允许这两种形式:

    1
    2
    3
    4
    5
    6
    7
    if (u)
        v;

    if (x) {
        y;
        z;
    }

    为此,对开口撑架进行了两种解释:1。不需要和2。它可以出现在任何地方。

    这是个错误的举动。真正的代码没有出现在任何地方的中间的左大括号,而真正的代码在使用第一种形式而不是第二种形式时也往往更脆弱。(大约每隔一个月,在我上一份工作中,当他们对我的代码的修改不起作用时,我会接到同事的电话,问题是他们在"if"中添加了一行,而没有添加大括号。我最终还是养成了这样的习惯:即使你只写一行,也总是需要大括号。)

    幸运的是,在许多情况下,eval()将复制javascript的完整功能。jsFiddle代码应为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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("[cc lang="javascript"]");
    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("

    "";< /代码>

    [这也是我多年来第一次写document.writeln,我觉得写任何涉及document.writeln()和eval()的东西都有点脏]


    在第二ventero"的解决方案。如果你想,你可以去享受到零售为两个+operands converts ITS技术。

    第一步:(§9.1)convert operands primitives(两个都是undefined原始值,null,布尔,字符串;数值是所有其他对象,包括数组和函数)。如果操作数。原始的冰,你是好的。如果不是,它是一个对象obj和接下来的步骤是:performed

  • obj.valueOf()呼叫。如果这一原始归来,你是好的。直接instances大学Object阵列和回报自己,所以,你是不好的。
  • obj.toString()呼叫。如果这一原始归来,你是好的。{}[]都返回一个字符串,那么你是好的。
  • 否则,把一TypeError
  • 对于约会,步骤1和2是swapped。你可以要守的转换为follows行为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var obj = {
        valueOf: function () {
            console.log("valueOf");
            return {}; // not a primitive
        },
        toString: function () {
            console.log("toString");
            return {}; // not a primitive
        }
    }

    (Number()第一converts互动的两个原始的前两个数):

    1
    2
    3
    4
    > Number(obj)
    valueOf
    toString
    TypeError: Cannot convert object to primitive value

    第二步(§11.6.1):如果一个字符串的operands冰,冰也其他操作数转换和结果两个字符串连接两市的冰产生的字符串。否则,两个都是operands转换结果数和冰淇淋生产增他们的城市。

    更详细的转换过程的解释:"什么是{ } { } +在JavaScript的?"


    我们两个可以参考的规范和《这是伟大的和最准确的,但必须是explained用例也可以在一个更comprehensible方式与下面的陈述:

    • +-经营者的工作只与原始值。更具体来说+(ADD)厂用它的字符串或数字,和+(一元)和-(subtraction鸭厂只与一元)数。
    • 全天然,函数或运营商的期望值作为原始参数,将这两个参数,第一convert desired原始类型。它是好的或与valueOftoString,这是在任何可用的对象。这是原因为什么这样的函数或运营商不要把错误的时候invoked在线数据。

    所以我们可以说,:

    • [] + []冰一样的String([]) + String([])AA AA '' + ''这是相同的。首先,在上述+(ADD)的冰也有效数,但有没有有效的阵列个数表示在母猪添加JavaScript字符串,而不是用冰。
    • [] + {}冰一样的String([]) + String({})AA AA '' + '[object Object]'这是相同的
    • {} + []。这一个有趣的deserves解释(见ventero答案)。在那卷曲的牙套,个案处理,是不是作为一个对象,但作为一个空座了,所以我会为+[]是相同的。一元+厂只与数,所以tries的实施得到了[]一号。第一,它tries valueOf的情况下返回相同的对象的数组,然后我它tries度假村:最后一个toString转换结果的两个数。我们可以为它写+Number(String([]))这是相同的+Number('')AA AA +0这是相同的。
    • Array(16).join("wat" - 1)subtraction -厂只与数,所以它的一样:Array(16).join(Number("wat") - 1)AA,AA "wat"不能转换的两个有效数字。我们收到NaN,和任何NaN算术操作的结果与NaN,所以我们有:Array(16).join(NaN)

    支持之前分享的东西。

    这种行为的根本原因部分是由于JavaScript的弱类型性。例如,表达式1+"2"不明确,因为基于操作数类型(int、string)和(int int int)有两种可能的解释:

    • 用户打算连接两个字符串,结果:"12"
    • 用户打算添加两个数字,结果:3

    因此,随着输入类型的变化,输出可能性增加。

    加法算法

  • 将操作数强制为基元值
  • javascript原语有string、number、null、undefined和boolean(ES6中很快就会出现符号)。任何其他值都是一个对象(例如数组、函数和对象)。将对象转换为基元值的强制过程如下所述:

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

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

    • 引发类型错误

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

  • 如果任何操作数值是字符串,则执行字符串串联

  • 否则,将两个操作数转换为它们的数值,然后添加这些值

  • 了解javascript中各种类型的强制值有助于使混淆的输出更加清晰。见下表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    +-----------------+-------------------+---------------+
    | 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",因为涉及字符串的任何加法将始终默认为字符串串联。

    你可以在这篇博文中阅读更多的例子(我写的免责声明)。