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

这是有效的,并返回JavaScript中的字符串"10"这里有更多的例子):

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

Why? What is happening here?

221728 次浏览

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

++[[]][+[]]+[+[]]

在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")。数字#0字符串#4字符串

"1" + "0" === "10" // Yay!

+[]的规格详细信息:

这是一个相当大的迷宫,但要做+[],首先它被转换为字符串,因为这就是+所说的:

11.4.6一元+运算符

一元+运算符将其操作数转换为数字类型。

生产UnaryExpress:+UnaryExpress的评估如下:

  1. 设exr为计算UnaryExpress的结果。

  2. 返回ToNumber(GetValue(exr))。

ToNumber()表示:

对象

应用以下步骤:

  1. 设primValue为ToPrimitive(输入参数,提示字符串)。

  2. 返回ToString(primValue)。

ToPrimitive()表示:

对象

返回对象的默认值。通过调用对象的[[DefaultValue]]内部方法来检索对象的默认值,传递可选提示PreferredType。[[DefaultValue]]内部方法的行为由本规范为8.12.8中的所有本机ECMAScript对象定义。

[[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.2Array.prototype.toString()

调用toString方法时,将执行以下步骤:

  1. 让数组是在this值上调用ToObject的结果。

  2. 假设func是调用带有参数“连接”的数组的[[Get]]内部方法的结果。

  3. 如果IsCallable(func)为false,则让func成为标准的内置方法Object.prototype.toString(15.2.4.2)。

  4. 返回调用func提供数组的[[Call]]内部方法的结果作为this值和一个空参数列表。

所以+[]归结为+"",因为[].join() === ""

同样,+定义为:

11.4.6一元+运算符

一元+运算符将其操作数转换为数字类型。

生产UnaryExpress:+UnaryExpress的评估如下:

  1. 设exr为计算UnaryExpress的结果。

  2. 返回ToNumber(GetValue(exr))。

ToNumber对于""定义为:

::的MV是0。

所以+"" === 0,因此+[] === 0

++[[]][+[]] => 1 // [+[]] = [0], ++0 = 1[+[]] => [0]

然后我们有一个字符串连接

1+[0].toString() = 10

+[]计算为0[…]然后用任何东西求和(+操作)它将数组内容转换为由逗号连接的元素组成的字符串表示。

任何其他像数组的索引(比+操作更优先)是有序的,没有什么有趣的。

以下内容改编自我在这个问题仍然关闭时发布的回答这个问题的博客文章。链接指向ECMAScript 3规范(超文本标记语言副本),它仍然是当今常用Web浏览器中JavaScript的基线。

首先,一个注释:这种表达式永远不会出现在任何(理智的)正式生产环境中,只能作为读者对JavaScript肮脏边缘的了解程度的练习。JavaScript运算符在类型之间隐式转换的一般原则是有用的,一些常见的转换也是有用的,但这种情况下的大部分细节不是。

表达式++[[]][+[]]+[+[]]最初可能看起来相当壮观和晦涩,但实际上相对容易分解成单独的表达式。为了清楚起见,我在下面简单地添加了括号;我可以向你保证它们没有任何改变,但如果你想验证这一点,请随时阅读有关分组操作符的信息。所以,表达式可以更清楚地写成

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

分解这一点,我们可以通过观察+[]的计算结果为0来简化。为了满足自己为什么这是真的,请查看一元+运算符并遵循稍微曲折的轨迹,最终ToPrimitive将空数组转换为空字符串,然后最终由总人数转换为0。我们现在可以为+[]的每个实例替换0

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

已经更简单了。至于++[[]][0],那是前缀增量运算符++)的组合,数组文字定义了一个具有单个元素的数组,该元素本身就是一个空数组([[]])和调用数组文字定义的数组的属性访问器[0])。

所以,我们可以将[[]][0]简化为[],我们有++[],对吗?事实上,情况并非如此,因为评估++[]会抛出一个错误,这最初看起来可能令人困惑。然而,稍微思考一下++的性质就可以清楚地表明这一点:它用于递增一个变量(例如++i)或一个对象属性(例如++obj.count)。它不仅评估为一个值,还会将该值存储在某个地方。在++[]的情况下,它无处放置新值(无论它可能是什么),因为没有对对象属性或变量的引用要更新。在规范术语中,这由内部推荐值操作覆盖,该操作由前缀增量运算符调用。

那么,++[[]][0]是做什么的呢?好吧,通过与+[]类似的逻辑,内部数组被转换为0,该值被1递增,从而得到1的最终值。外部数组中属性0的值更新为1,整个表达式的计算结果为1

这给我们留下了

1 + [0]

…这是对加法运算符的简单使用。两个操作数都是第一个转换为原语,如果其中一个原始值是字符串,则执行字符串连接,否则执行数字加法。[0]转换为"0",因此使用字符串连接,产生"10"

最后,可能不会立即明显的是,覆盖Array.prototypetoString()valueOf()方法之一将更改表达式的结果,因为在将对象转换为原始值时,如果存在,则会检查并使用这两个方法

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

…产生"NaNfoo"。为什么会发生这种情况留给读者做练习…

这个评估结果是一样的但是小一点

+!![]+''+(+[])
  • []-是一个数组转换,当您添加或减去它时转换为0,因此 +[] = 0
  • ![]-计算为false,因此!![]计算为true
  • +!![]-将true转换为计算结果为true的数值,因此在本例中为1
  • +"-将一个空字符串附加到表达式中,导致数字转换为字符串
  • +[]-计算为0

所以被评价为

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

现在你知道了,试试这个:

_=$=+[],++_+''+$
  1. 一元加给定字符串转换为数字
  2. 递增运算符给定的字符串转换和递增1
  3. [] == ''. 空字符串
  4. +"或+[]计算0。

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

让我们简单一点:

++[[]][+[]]+[+[]] = "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"

也许将表达式评估为"10"而没有数字的最短可能方法是:

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

补充说明

  • +!+[]
    • +[]被评估为0
    • !0被评估为true
    • +true被评估为1
  • -~[]-(-1)相同,计算结果为1
  • [+[]]
    • +[]被评估为0
    • [0]是一个包含单个元素0的数组。

然后,JS评估1 + [0],一个数量+数组表达式。然后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,因为一元+将对象强制转换为数字。

一步一步地,+将值转换为一个数字,如果您添加到一个空数组+[]…因为它是空的并且等于0,它将

所以从那里,现在看看你的代码,它是++[[]][+[]]+[+[]]

它们之间有正数++[[]][+[]]+[+[]]

因此,这些[+[]]将返回[0],因为它们有一个空数组,该数组在另一个数组中被转换为0

所以想象一下,第一个值是一个二维数组,里面有一个数组……所以[[]][+[]]将等于[[]][0],它将返回[]……

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

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

为什么返回字符串

++[[]][+[]]+[+[]]^^^|v++[[]][+[]]+[0]^^^|v++[[]][0]+[0]^^^^^^^|v++[]+[0]^^^|v++[]+"0"^^^^|v++0+"0"^^^|v1+"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的数字相同。

如果任何操作数是字符串,则+连接。