/ 兰林的博客 / 屌丝 JavaScript 之怪异的框框 []

屌丝 JavaScript 之怪异的框框 []

2013-02-19 posted in [程序语言]

你能一下子就说出下面这段代码的值吗?

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

如果我告诉你,这段代码的值是 10 你会惊讶吗?

其实我一开始也惊讶了,不过仔细想了想好像也没什么。

不过我觉得有时候可以用这种形式的代码来实现某些“隐藏值”,或者是纯粹为了装逼。

因为这样往往能让初次碰到的人大吃一惊,可能还会对你顶礼膜拜哦。

扯远了,其实这段代码的关键在于————弱类型语言的自动类型转换

首先我们将上面的那段代码拆分成两段

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

然后我们提取出其中的

+[]    // 这是将要被转换的代码

js会把 +[] 转换为 0,注意类型为整形,而非字符型。通过下面的方式可以证明

+[] === 0    // 返回 true
+'' === 0    // 同样返回 true
+[7] === 7   // 还是返回 true

那么上面的代码就等同于

++[[]][0]
+[0]

再来看 [[]][0],很多人对此的解释是————这等同于取出数组的第一个元素,也就是取出了 []。

这个也对也不对,主要是没有人对下面这段代码作出过详细的解释

++[]        // 出错
++[]+[0]    // 还是出错

那么为什么 ++[[]][0] 能返回整形的 1,而 ++[] 就会报错呢?

首先,我们来看 ++[] 的错误信息

ReferenceError: Invalid left-hand side expression in prefix operation
// 引用错误:前缀操作中的左侧表达式无效。 

这个错误是引用规范中提到的抽象操作 PutValue (V, W) 被调用时抛出的。

当 ++[] 执行时,PutValue 就会被调用。

PutValue 要求要有一个引用,而 ++[] 中的 [] 本身只是一个表达式,而不是一个引用,

因此会报错。这是非常好理解的,我们可以设想一下 ++[] 会在 [] 的基础上加 1,

假如 [] 的内存地址是 9527 但是新的值存放在哪里呢?不可能直接替换掉 9527 吧?

所以必须用一个引用来指向这个新值的内存地址(姑且假设新值的内存地址为 9528 吧)。

那么我们加上引用再试一下

var a = [];
++a;           // 返回结果 1

由此可以证明确实是缺少引用的问题。但是为什么 ++[[]][0] 不会报错呢?

这个就是我为什么说前面的说法也对也不对了。

[[]] 是一个对象这个大家肯定都没有疑问,而 [[]][0] 显然是访问了 [[]] 这个

对象的第一个属性吧?

那么也就是说 “0” 引用了 [[]] 中的第一个属性 []。既然引用有了那么接下来的

操作就和 var a = []; 没有什么分别了。

++[[]][0] === 1 // 返回 true
1 + [0]         // 返回 "10"

最后再回过头来说说 js 自动类型转换的一些规则,由此来解释

为什么 1 + [0] 结果会是 10.

首先是 ++ 的问题,对于引用的是对象的这种,比如前面的 “0” 引用 [],或者

a 引用 [],会执行以下步骤的操作

a.toString();    // 返回 "" ,一个空字符串
++"";            // 返回整形的 1

接下来是 1 + [0] 为什么是 “10” 而不是 1 的问题了。

我们前面说了 +[] 和 +[0] 得到的是整形的 0,但是这里为什么会变成

了字符型的 “0”。

原因很简单 +[] 的时候 + 不是表示加号,而是表示正负的正号。

[] 是一个对象,会调用 toString() 转换为 “”.

而 +”” 会得到结果整形的 0。

而这里的 1 + [0] 究竟是加号还是连字符

这要取决于 [0] 的转换结果。

[0].toString();    // 返回的结果为字符型的 "0"

1 + "0"            // 返回字符串 "10"

这个是因为数字 + 字符串时,+ 会作为连字符对待,把表达式的两个因子拼接到一起。

OK,至此算是把这段代码的前世今生分析清楚了,阿弥陀佛。。。。

更多框框里个框框,请参见Java/script: no alnum cheat sheets

关于引用的说明请查看引用规范.

关于js的类型转换,请参考Javascript类型转换的规则中的–隐式转换.