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

CodeMash 2012的Wat演讲基本上指出了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

做wat+1的结果是wat1wat1wat1wat1

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

72617 次浏览

这是你看到的(和应该看到的)结果的解释列表。我使用的引用来自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]"(参见[] + {})。因此,为了得到+{}的结果,我们必须在字符串"[object Object]"上应用ToNumber。按照+{}0的步骤进行操作时,我们得到NaN

    如果语法不能将String解释为字符串类型的扩展,则总人数的结果是NaN

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

    根据§15.4.1.1§15.4.2.2Array(16)创建一个长度为16的新数组。为了获得要连接的参数的值,§11.6.2步骤§15.4.1.1§15.4.2.2表明我们必须使用ToNumber将两个操作数转换为数字。ToNumber(1)只是1(§9.3),而ToNumber("wat")再次是NaN,按照§9.3.1。在§11.6.2的步骤7之后,ToNumber1规定

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

    所以Array(16).join的参数是NaN。在§15.4.4.5(Array.prototype.join)之后,我们必须对参数调用ToString,即"NaN"§9.8.1):

    如果mNaN,返回字符串"NaN"

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

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

我支持@Ventro的解决方案。如果你愿意,你可以更详细地了解+如何转换它的操作数。

第一步(§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)valueOftoStringTypeError: Cannot convert object to primitive value

第二步(§11.6.1):如果其中一个操作数是字符串,则另一个操作数也会转换为字符串,并通过连接两个字符串产生结果。否则,两个操作数都将转换为数字,并通过将它们相加产生结果。

转换过程的更详细说明:“JavaScript中的 {} + {} 是什么?

我们可以参考规范,这是伟大和最准确的,但大多数情况也可以通过以下陈述以更容易理解的方式解释:

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

所以我们可以说:

  • [] + []String([]) + String([])相同,与'' + ''相同。我上面提到+(加法)也对数字有效,但JavaScript中没有数组的有效数字表示,因此使用字符串加法代替。
  • [] + {}String([]) + String({})相同,与'' + '[object Object]'相同
  • {} + []。这个问题需要更多的解释(参见文特罗的回答)。在这种情况下,花括号不被视为对象,而是被视为空块,因此它与+[]相同。一元+仅适用于数字,因此实现尝试从[]中获取数字。首先它尝试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)

这与其说是一个答案,不如说是一个评论,但由于某种原因,我不能对你的问题发表评论。我想更正你的JSFiddle代码。然而,我在黑客新闻上发布了这个,有人建议我在这里重新发布。

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

基本思想很简单,JavaScript希望允许这两种形式:

if (u)v;
if (x) {y;z;}

为了做到这一点,对开括号进行了两种解释:1.它是不需要和2.它可以出现任何地方

这是一个错误的举动。真正的代码不会突然出现大括号,当真正的代码使用第一种形式而不是第二种形式时,它也会变得更加脆弱。(在我的上一份工作中,大约每隔一个月,我就会因为同事对我的代码所做的修改不起作用而被叫到他们的办公桌前,问题是他们在“if”后面加了一行而没有加花括号。最终,我养成了一个习惯,即使你只写了一行,也总是需要花括号。)

幸运的是,在许多情况下,ava()会复制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()和ava()的东西都有点脏。

支持之前分享的内容。

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

  • 用户打算连接两个字符串,结果:“12”
  • 用户打算将两个数字相加,结果:3

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

加法算法

  1. 强制操作数为原始值

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

  • 如果在调用object.valueOf()时返回基元值,则返回此值,否则继续

  • 如果在调用object.toString()时返回基元值,则返回此值,否则继续

  • 抛出一个TypeError

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

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