'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 的简单行为。
这是您所看到(并且应该看到)结果的解释列表。我使用的参考来自 ECMA-262 standard。
[] + [] 使用加法运算符时,左操作数和右操作数都首先转换为基元(第 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:如果文法不能将 String 解释为 StringNumericLiteral 的扩展,那么 ToNumber 的结果是 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 规定如果任一操作数为 NaN,则结果为 NaN。所以 Array(16).join 的参数是 NaN。按照第 15.4.4.5 节(Array.prototype.join),我们必须在参数上调用 ToString,即“NaN”(第 9.8.1 节):如果 m 是 NaN,则返回字符串“NaN”。在第 15.4.4.5 节的第 10 步之后,我们得到 15 次重复“NaN”和空字符串的串联,这等于您看到的结果。当使用 "wat" + 1 而不是 "wat" - 1 作为参数时,加法运算符将 1 转换为字符串而不是将 "wat" 转换为数字,因此它有效地调用 Array(16).join("wat1") .
至于为什么您会在 {} + []
案例中看到不同的结果:当使用它作为函数参数时,您将语句强制为 ExpressionStatement,这使得无法解析 {2 } 作为空块,因此它被解析为空对象文字。
这更像是评论而不是答案,但由于某种原因,我无法评论您的问题。我想更正您的 JSFiddle 代码。然而,我在 Hacker News 上发布了这个,有人建议我在这里重新发布。
JSFiddle 代码中的问题是 ({})
(括号内的左大括号)与 {}
(左大括号作为代码行的开头)不同。因此,当您输入 out({} + [])
时,您将强制 {}
成为您输入 {} + []
时没有的东西。这是 Javascript 的整体 'wat'-ness 的一部分。
基本思想很简单,JavaScript 希望允许这两种形式:
if (u)
v;
if (x) {
y;
z;
}
为此,对左大括号进行了两种解释:1. 它不是必需的,2. 它可以出现在任何地方。
这是一个错误的举动。真正的代码不会有一个左大括号出现在不知名的地方,而且当它使用第一种形式而不是第二种形式时,真正的代码也往往更脆弱。 (在我的上一份工作中,大约每隔一个月一次,当他们对我的代码的修改不起作用时,我会被叫到同事的办公桌,问题是他们在“if”中添加了一行而没有添加 curly大括号。我最终只是养成了总是需要花括号的习惯,即使你只写一行。)
幸运的是,在许多情况下 eval() 将复制 JavaScript 的全部功能。 JSFiddle 代码应为:
function out(code) {
function format(x) {
return typeof x === "string" ?
JSON.stringify(x) : x;
}
document.writeln('>>> ' + 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()的东西我都觉得有点脏。]
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere
- 我不同意(有点):过去我经常使用这样的块来限定在 C 中的变量。这个习惯在做嵌入式 C 时被拾起,因为堆栈上的变量占用空间,所以如果不再需要它们,我们希望在块的末尾释放空间。但是,ECMAScript 仅适用于 function(){} 块。所以,虽然我不同意这个概念是错误的,但我同意 JS 中的实现是(可能)错误的。
let
来声明块范围的变量。
我支持@Ventero 的解决方案。如果您愿意,可以更详细地了解 +
如何转换其操作数。
第一步(第 9.1 节): 将两个操作数都转换为原始值(原始值是 undefined
、null
、布尔值、数字、字符串;所有其他值都是对象,包括数组和函数)。如果一个操作数已经是原始的,那么你就完成了。如果不是,它是一个对象 obj
并执行以下步骤:
调用 obj.valueOf()。如果它返回一个原语,你就完成了。 Object 和数组的直接实例返回它们自己,所以你还没有完成。调用 obj.toString()。如果它返回一个原语,你就完成了。 {} 和 [] 都返回一个字符串,这样就完成了。否则,抛出一个 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 节):如果其中一个操作数是字符串,则另一个操作数也转换为字符串,并通过连接两个字符串产生结果。否则,两个操作数都将转换为数字,并通过将它们相加产生结果。
转换过程的更详细说明:“What is {} + {} in JavaScript?”
我们可以参考规范,这很好,最准确,但大多数情况也可以用以下陈述以更易于理解的方式解释:
+ 和 - 运算符仅适用于原始值。更具体地说,+(加法)适用于字符串或数字,而 +(一元)和 -(减法和一元)仅适用于数字。
所有期望原始值作为参数的本机函数或运算符,将首先将该参数转换为所需的原始类型。它是通过 valueOf 或 toString 完成的,它们在任何对象上都可用。这就是为什么在对象上调用此类函数或运算符时不会抛出错误的原因。
所以我们可以说:
[] + [] 等同于 String([]) + String([]) 等同于 '' + ''。我在上面提到 +(addition) 对数字也有效,但是 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)。
支持之前分享的内容。
这种行为的根本原因部分是由于 JavaScript 的弱类型特性。例如,表达式 1 + “2” 是不明确的,因为基于操作数类型 (int, string) 和 (int int) 有两种可能的解释:
用户打算连接两个字符串,结果:“12”
用户打算将两个数字相加,结果:3
因此,随着输入类型的变化,输出的可能性会增加。
加法算法
将操作数强制为原始值
JavaScript 原语是 string、number、null、undefined 和 boolean(Symbol 即将在 ES6 中推出)。任何其他值都是对象(例如数组、函数和对象)。将对象转换为原始值的强制过程如下所述:
如果调用 object.valueOf() 时返回原始值,则返回该值,否则继续
如果调用 object.toString() 时返回原始值,则返回该值,否则继续
抛出类型错误
注意:对于日期值,顺序是在 valueOf 之前调用 toString。
如果任何操作数值是字符串,则进行字符串连接否则,将两个操作数转换为它们的数值,然后将这些值相加
了解 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",因为任何涉及字符串的加法都将始终默认为字符串连接。
您可以在 this blog post 中阅读更多示例(我写的免责声明)。
[]+1
几乎遵循与[]+[]
相同的逻辑,只是将1.toString()
作为 rhs 操作数。对于[]-1
,请参见第 5 点中对"wat"-1
的解释。请记住ToNumber(ToPrimitive([]))
为 0(第 3 点)。