为什么 ++[[]][+[]]+[+[]] 返回字符串 "10"?

1886
这是在JavaScript中有效的代码,并返回字符串"10"这里有更多示例):

console.log(++[[]][+[]]+[+[]])

为什么?这里发生了什么?


542
首先要理解的是,+[] 将一个空数组转换为 0... 然后就会浪费一个下午时间... ;) - deceze
17
相关链接:https://dev59.com/8W855IYBdhLWcg3wvXDd。请说明为什么这个代码可以运行。 - Juho Vepsäläinen
15
请浏览http://wtfjs.com/,它有很多类似的例子,并附有解释。 - ThiefMaster
5
这些并不是你在正常编程中应该使用的东西。它只是鸭子类型和隐式转换的怪癖。你可能已经看到过,你可以使用将数字转换为布尔值。你可以使用+将布尔值或字符串转换为数字。从那里开始,你需要弄清楚哪些值会返回真值(强制转换为true或1),哪些值会返回假值(强制转换为false或0)。 - Marie
10个回答

2268

如果我们把它分开,混乱就等于:

++[[]][+[]]
+
[+[]]
在JavaScript中,+[] === 0是成立的。+会将某些东西转换为数字,在这种情况下,它将转化为+""0(请参见下面的规范细节)。
因此,我们可以简化它(++ 的优先级高于 +):
++[[]][0]
+
[0]

因为 [[]][0] 的意思是:从 [[]] 中获取第一个元素,所以以下语句成立:

[[]][0] 返回内部的数组([])。由于引用的原因,不能说 [[]][0] === [],但我们可以称内部的数组为A以避免这种错误的表示法。

在其操作数之前使用++表示“增加1并返回增加后的结果”。所以++[[]][0]等同于Number(A) + 1(或+A + 1)。

再次简化,我们可以将A替换回[]

(+[] + 1)
+
[0]

+[] 可以将数组强制转换成数字 0 之前,它需要先被强制转换为字符串,即为 ""。最后加上 1,结果就是 1

  • (+[] + 1) === (+"" + 1)
  • (+"" + 1) === (0 + 1)
  • (0 + 1) === 1

让我们再简化一下:

1
+
[0]

此外,在JavaScript中也是这样的:[0] == "0",因为它将一个只有一个元素的数组连接起来。连接操作会将元素用,分隔并拼接在一起。由于只有一个元素,你可以推断出这个逻辑会返回第一个元素本身。

在这种情况下,+ 看到两个操作数:一个数字和一个数组。现在它试图将两个操作数强制转换为相同的类型。首先,数组被强制转换为字符串 "0",然后,数字被强制转换为字符串("1")。Number + String === String

"1" + "0" === "10" // Yay!
+[]的规范细节如下:
首先要将+[]转换为字符串,因为+运算符是这样规定的:
"11.4.6一元+运算符
一元+运算符将其操作数转换为Number类型。
语法:一元表达式 + UnaryExpression
执行如下步骤:
1. 让expr成为求值UnaryExpression的结果。 2. 返回ToNumber(GetValue(expr))。" ToNumber()定义如下:
"对象
执行以下步骤:
1. 让primValue成为调用ToPrimitive时使用的输入参数和hint String(字符串)的结果。 2. 返回ToString(primValue)。" ToPrimitive()定义如下:
"对象
返回对象的默认值。通过调用对象的[[DefaultValue]]内部方法并传递可选提示PreferredType来检索对象的默认值。所有本地ECMAScript对象的 [[DefaultValue]] 内部方法的行为在8.12.8中被本规范定义。" [[DefaultValue]]定义如下:
"8.12.8 [[DefaultValue]] (hint)
当使用提示String调用O的[[DefaultValue]]内部方法时,执行以下步骤:
1. 让toString成为调用带有参数“toString”的对象O的[[Get]]内部方法的结果。 2. 如果IsCallable(toString)为true,则: a. 让str成为调用toString的[[Call]]内部方法的结果,该方法将O作为this值和一个空参数列表。 b. 如果str是原始值,则返回str。"
数组的.toString定义如下:
"15.4.4.2 Array.prototype.toString ( )
调用toString方法时,执行以下步骤:
1. 让array成为对this值调用ToObject的结果。 2. 让func成为调用带有参数“join”的数组的[[Get]]内部方法的结果。 3. 如果IsCallable(func)为false,则将func设置为标准内置方法Object.prototype.toString(15.2.4.2)。 4. 返回提供数组作为this值和一个空参数列表的func的[[Call]]内部方法的结果。
因此,+[]可以简化为+"",因为[].join() === ""
再次强调,+定义如下:
"11.4.6 一元+运算符
一元+运算符将其操作数转换为Number类型。
语法:一元表达式 + UnaryExpression
执行如下步骤:
1. 让expr成为求值UnaryExpression的结果。 2. 返回ToNumber(GetValue(expr))。"
针对""ToNumber定义如下:
"StringNumericLiteral ::: [empty] 的MV为0。"
所以+"" === 0,因此+[] === 0

9
@harper: 这是一个严格的相等性检查器,即只有在值和类型都相同的情况下才会返回true0 == ""将返回true(经过类型转换后相同),但是0 === ""将返回false(类型不同)。 - pimvdb
50
部分内容不正确。这个表达式简化为1 + [0],而不是"1" + [0],因为前缀(++)操作符总是返回一个数字。参考链接:http://bclary.com/2004/11/07/#a-11.4.4 - Tim Down
8
@Tim Down:你说得完全正确。我正在尝试更正这个问题,但在这样做时我发现了别的问题。我不确定这是如何可能的。++[[]][0]确实返回1,但++[]会抛出一个错误。这很引人注目,因为它看起来好像++[[]][0]归结为++[]。你是否有任何想法,为什么++[]会抛出错误,而++[[]][0]却不会? - pimvdb
15
我很确定问题在前缀操作的 PutValue 调用中(在 ES3 术语下为 8.7.2)。PutValue 需要一个引用,而 [] 作为一个单独的表达式不会产生引用。包含变量引用的表达式(比如我们之前定义了 var a = [],那么 ++a 就可以工作),或者对象属性访问(比如 [[]][0]),会产生一个引用。简单地说,前缀运算符不仅会产生一个值,还需要一个位置来存放该值。 - Tim Down
14
执行var a = []; ++a后,a的值为1。执行++[[]][0]后,由[[]]创建的数组现在只包含索引为0的数字1。++需要一个引用来完成此操作。 - Tim Down
显示剩余9条评论

136
  • ++[ [] ][+[]] === 1
    • +[] === 0
    • ++[ [] ][0] === 1
  • [ +[] ] 等于 [ 0 ]

然后我们有一个字符串拼接:

1 + String([ 0 ]) === 10


73
以下内容是从我在此问题尚未关闭时发布的博客文章中回答这个问题所改编的。链接指向(ECMAScript 3规范的HTML副本),仍然是今天常用的Web浏览器中JavaScript的基线。
首先,需要说明的是:这种表达式永远不会出现在任何(理智的)生产环境中,只有作为一种练习,检验读者对JavaScript的边缘知识掌握得如何,才有些许用处。JavaScript运算符隐式地转换类型的一般原则是有用的,一些常见的转换也是有用的,但在这种情况下的大部分细节是没有用的。
表达式++[[]][+[]]+[+[]]可能看起来相当令人生畏和晦涩,但实际上可以相对容易地将其分解为单独的表达式。以下是我为了清晰而添加的括号。我可以向您保证它们没有改变任何东西,但如果您想要验证,请自由阅读分组运算符。因此,该表达式可以更清晰地写成:
( ++[[]][+[]] ) + ( [+[]] )

将其分解,我们可以通过观察得出+[]的值为0。要满足自己这是为什么,可以查看一元+运算符并跟随略微曲折的路径,最终ToPrimitive将空数组转换为空字符串,然后ToNumber将其转换为0。现在我们可以将每个+[]实例替换为0

( ++[[]][0] ) + [0]

更简单了。至于++[[]][0],它是前缀递增运算符++)、一个定义了只有一个元素的数组,该元素本身是一个空数组的数组字面量[[]])和在由数组字面量定义的数组上调用的属性访问器[0])的组合。
所以,我们可以将[[]][0]简化为[],然后就有了++[],对吧?实际上不是这样的,因为评估++[]会抛出错误,这可能一开始看起来很令人困惑。然而,对++的本质进行一些思考就会变得清晰:它用于递增变量(例如++i)或对象属性(例如++obj.count)。它不仅评估为一个值,还将该值存储在某个地方。在++[]的情况下,它没有任何地方可以放置新值(无论它是什么),因为没有引用要更新的对象属性或变量。在规范术语中,这由内部的PutValue操作覆盖,该操作由前缀递增运算符调用。
那么,++[[]][0] 是什么意思呢?好吧,根据与 +[] 类似的逻辑,内部数组被转换为 0,这个值加 1 得到最终值 1。外部数组中属性 0 的值更新为 1,整个表达式评估为 1
这使我们得到
1 + [0]

...这是加法运算符的一个简单用法。两个操作数首先被转换为原始值,如果任何一个原始值是字符串,则执行字符串连接,否则执行数字相加。 [0] 转换为 "0",因此使用字符串连接,产生 "10"

最后需要说明的是,可能不太明显的一点是,覆盖 Array.prototypetoString()valueOf() 方法之一将更改表达式的结果,因为在将对象转换为原始值时,如果存在这两个方法,则会检查并使用它们。例如,以下内容:

Array.prototype.toString = function() {
  return "foo";
};
++[[]][+[]]+[+[]]

... 会生成 "NaNfoo"。为什么会这样发生留给读者自己思考...


31

让我们简单明了:

++[[]][+[]]+[+[]] = "10"

var a = [[]][+[]];
var b = [+[]];

// so a == [] and b == [0]

++a;

// then a == 1 and b is still that array [0]
// when you sum the var a and an array, it will sum b as a string just like that:

1 + "0" = "10"

16

这个表达式的值相同,但稍微小一些

+!![]+''+(+[])
  • [] - 数组在加减运算时转换为0,所以 +[] = 0
  • ![] - 转换成 false,所以 !![] 转换成 true
  • +!![] - 将 true 转换为数字值,其值为 1
  • +'' - 向表达式中添加一个空字符串,导致数字被转换为字符串
  • +[] - 等同于 0

因此,它的求值结果为:

+(true) + '' + (0)
1 + '' + 0
"10"

现在你已经了解了那个,试试这个:

_=$=+[],++_+''+$

好的,它仍然评估为“10”。但是这是用不同的方式来做的。尝试在像Chrome之类的JavaScript检查器中评估此内容。 - Vlad Shlosberg
=$=+[],+++''+$ -> =$=0,+++''+$ -> =0,$=0,+++''+$ -> ++0+''+0 -> 1+''+0 -> '10' // 嗱呢 :v - LeagueOfJava
6
这个表达式的结果与你的相同,但它比你的更小:"10" - ADJenks

10
++[[]][+[]]+[+[]]
             ^^^
             |
             v
++[[]][+[]]+[0]
       ^^^
       |
       v
++[[]][0]+[0]
  ^^^^^^^
  |
  v
++[]+[0]
     ^^^
     |
     v
++[]+"0"
^^^^
|
v
++0+"0"
^^^
|
v
1+"0"
^^^^^
|
v
"10"

+ 操作符通过 .valueOf() 强制将非数字操作数转换为数字。如果该方法不返回数字,则会调用 .toString() 方法。

我们可以通过以下方式进行验证:

const x = [], y = [];
x.valueOf = () => (console.log('x.valueOf() has been called'), y.valueOf());
x.toString = () => (console.log('x.toString() has been called'), y.toString());
console.log(`+x -> ${+x}`);

+[] 相当于把空字符串 "" 强制转换为数字,即 0

如果操作数中有一个字符串,则 + 会进行字符串拼接。


我发现 ++[]+"0" 是无效的,因为第一部分 ++[] 会导致错误。然而,如果有一个索引,比如 ++[][0],那么它是允许的。 - qrsngky

8

+[]的值为0。 [...]然后与任何东西一起求和(+操作),将会把数组内容转换为由逗号拼接的字符串表示形式。

对于数组取索引之类的操作(其优先级高于+操作),这是普通的操作,没有什么有趣的。


8

不使用数字来评估表达式“10”的最短可能方式可能是:

+!+[] + [+[]] // "10"
-~[] + [+[]]  // "10"

解释

  • +!+[]
    • +[]被求值为0
    • !0被求值为true
    • +true被求值为1
  • -~[]等同于-(-1),被求值为1
  • [+[]]:
    • +[]被求值为0
    • [0]是一个只含有一个元素0的数组。

然后,JS会求值1 + [0],一个Number + Array表达式。然后根据ECMA规范进行操作:+运算符通过调用ToPrimitiveToString抽象运算将两个操作数都转换为字符串。如果表达式的两个操作数只是数字,则它们会执行加法运算。这个技巧是数组可以轻松地将其元素强制转换为连接的字符串表示。

以下是一些例子:

1 + {}            // "1[object Object]"
1 + []            // "1"
1 + new Date()    // "1Wed Jun 19 2013 12:13:25 GMT+0400 (Caucasus Standard Time)"
[] + []           // ""
[1] + [2]         // "12"
{} + {}           // "[object Object][object Object]" ¹
{a:1} + {b:2}     // "[object Object][object Object]" ¹
[1, {}] + [2, {}] // "1,[object Object]2,[object Object]"

¹: 请注意,每行代码都是在表达式上下文中进行评估的。首个 {} 是一个 对象字面量,而不是语句上下文中的块。在 REPL 中,您可能会看到 {} + {} 的结果是 NaN,因为大多数 REPL 操作都在语句上下文中;这里,第一个 {} 是一个 ,而且该代码等效于 {}; +{},其中最终表达式语句(其值成为完成记录的结果)是 NaN,因为一元运算符 + 将对象强制转换为数字。


6

按照步骤,+会将值转换为数字,如果你想把它加到一个空数组上+[]...由于这个数组是空的且等于0,所以它将会

现在看看你的代码,它是这样的++[[]][+[]]+[+[]]...

它们之间有加号++[[]][+[]] + [+[]]

所以这些[+[]]将返回[0],因为它们包含的是一个空数组,而这个空数组会被转换为0放在另一个数组里...

所以可以想象,第一个值是一个有一个数组的二维数组...所以[[]][+[]]将等于[[]][0],它将返回[]...

最后,++将其转换并增加到1...

所以你可以想象,1 + "0"将会是"10"...

为什么会返回字符串“10”?


3
  1. Unary plus given string converts to number
  2. Increment operator given string converts and increments by 1
  3. [] == ''. Empty String
  4. +'' or +[] evaluates 0.

    ++[[]][+[]]+[+[]] = 10 
    ++[''][0] + [0] : First part is gives zeroth element of the array which is empty string 
    1+0 
    10
    

2
答案很混乱,换句话说是错误的。[]不等同于""。首先提取元素,然后通过++进行转换。 - PointedEars

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