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

IT技术 javascript
2021-01-18 23:18:09

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 的简单行为。

5个回答

以下是对您所看到(并且应该看到)结果的解释列表。我使用的参考来自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.1ToNumber("")是 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 表明我们必须将两个操作数转换为使用ToNumber. ToNumber(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,这使得它无法解析{}为空块,所以它被解析为一个空对象文字。

@RobElsner[]+1几乎遵循与 相同的逻辑[]+[],只是使用1.toString()rhs 操作数。[]-1看到的说明"wat"-1在点5记住,ToNumber(ToPrimitive([]))是0(点3)。
2021-03-22 23:18:09
那么为什么 []+1 => "1" 和 []-1 => -1 呢?
2021-04-07 23:18:09
这个解释缺少/省略了很多细节。例如,“将对象(在本例中为数组)转换为原始值会返回其默认值,对于具有有效 toString() 方法的对象来说,这是调用 object.toString() 的结果”完全缺少 [] 的 valueOf 是首先调用,但因为返回值不是原始值(它是一个数组),所以使用 [] 的 toString 代替。我建议看这个,而不是真正深入的解释2ality.com/2012/01/object-plus-object.html
2021-04-08 23:18:09

这与其说是回答,不如说是评论,但出于某种原因,我无法对您的问题发表评论。我想更正您的 JSFiddle 代码。然而,我在 Hacker News 上发布了这个,有人建议我在这里重新发布。

JSFiddle 代码中的问题在于({})(括号内的大括号)与{}(大括号作为一行代码的开头)不同。因此,当您键入时,out({} + [])您是在强迫 成为{}您键入时并非如此的东西{} + []这是 Javascript 整体“wat”的一部分。

基本思想是简单的 JavaScript 想要允许这两种形式:

if (u)
    v;

if (x) {
    y;
    z;
}

为此,对左大括号进行了两种解释:1. 它不是必需的,2. 它可以出现在任何地方

这是一个错误的举动。真正的代码没有出现在任何地方的左括号,而且当真正的代码使用第一种形式而不是第二种形式时,它也往往更脆弱。(在我上一份工作中大约每隔一个月一次,当他们对我的代码的修改不起作用时,我会被叫到同事的办公桌上,问题是他们在“if”中添加了一行而没有添加 curl大括号。我最终养成了始终需要大括号的习惯,即使您只写一行。)

幸运的是,在许多情况下 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()的东西都有些脏。]

@JessTelford 在 ES6 中,您可以使用let声明块范围的变量。
2021-03-15 23:18:09
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- 我不同意(有点):我过去经常使用这样的块来定义C 中的变量范围这种习惯是在执行嵌入式 C 时养成的,其中堆栈上的变量占用空间,因此如果不再需要它们,我们希望在块的末尾释放空间。但是,ECMAScript 仅作用于 function(){} 块内。因此,虽然我不同意这个概念是错误的,但我同意 JS 中的实现(可能)是错误的。
2021-04-08 23:18:09

我第二个@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 中的 {} + {} 是什么?

我们可以参考规范,这很好也最准确,但大多数情况也可以通过以下语句以更易于理解的方式进行解释:

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

所以我们可以说:

  • [] + []与 相同 与String([]) + String([])相同'' + ''我在上面提到过+(addition) 也对数字有效,但 JavaScript 中没有数组的有效数字表示,因此使用字符串相加代替。
  • [] + {}相同于String([]) + String({})相同于'' + '[object Object]'
  • {} + []. 这个值得更多解释(见 Ventero 回答)。在这种情况下,花括号不被视为一个对象,而是一个空块,因此结果与+[]. 一元+仅适用于数字,因此实现尝试从[]. 首先它尝试valueOf在数组的情况下哪个返回相同的对象,然后它尝试最后的手段:将toString结果转换为数字。我们可以把它写成+Number(String([]))which is same as +Number('')which is same as +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

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

加法算法

  1. 将操作数强制为原始值

JavaScript 原语是字符串、数字、空值、未定义和布尔值(符号即将在 ES6 中推出)。任何其他值都是一个对象(例如数组、函数和对象)。将对象转换为原始值的强制过程描述如下:

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

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

  • 抛出类型错误

注意:对于日期值,顺序是在 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",因为任何涉及字符串的加法将始终默认为字符串连接。

您可以在这篇博文中阅读更多示例(免责声明)。