事由
前几天有人在微博上发了一张图片(回微博找没找到了),说对于下面的运算较为困惑:
[]+{}=== {}+[] // true
{}+[]=== []+{} // false
({}+[]) === ([]+{}) // true
[]+{} // "[object Object]"
{}+[] // 0
({}+[]) // "[object Object]"
当时我看了题目之后,兴趣与疑心并存,并产生了强烈的好奇感,很奇怪的问题,不是吗?而且,它与eval('{a:1}')不会报错而eval('{“a”:1}')就会报错又有什么关系呢?疑点重重不是吗?
实验
带着好奇心,我在Chrome控制台做了一番实验,发现结果果然是这样!
但是就在我更疑惑不解的时候,我突然想起了一个问题,Chrome的控制台是用eval来执行的!于是,我马上在编译器里输入了一下测试代码,结果果然不出我所料:
[]+{}=== {}+[] // true
{}+[]=== []+{} // true
({}+[]) === ([]+{}) // true
[]+{} // "[object Object]"
{}+[] // "[object Object]"
({}+[]) // "[object Object]"
于是,一切疑问烟消云散!
解惑
toString()与valueOf()
首先,需要解释下Object原型的toString()和valueOf()方法。
对象通过隐式的调用其自身的toString方法将对象转换为字符串。你可以调用对象的toString()方法进行测试。
Math.toString(); // "[object Math]"
JSON.toString(); // "[object JSON]"
类似的,对象也可以通过其valueOf方法转换为数字。通过下面类似的方法,你可以控制对象的类型转换:
“J”+{toString:function(){return "S";}};//"JS"
2*{valueOf:function(){return 5;}}// 10
当你认识到运算符+被重载来实现字符串链接和加法的时候,事情变得棘手起来。特别的,当一个对象同时包含toString和toValueOf的时候,运算符+应该调用哪个方法并不明显。JS盲目的选择valueOf方法而不是toString方法来解决这一问题。
var obj = {
toString:function(){
console.error('Object toString');
return "s";
},
valueOf:function(){
console.error('Object valueOf');
return 2;
}
};
"object:"+obj; // "object:2"
这个例子说明,valueOf()方法才真正是为那些代表数值的对象(如Number对象)而设计的。因此,最好避免使用valueOf()方法。当对象没有明确指定toString()或valueOf()方法的时候(上面的代码为明确指定)则默认会调用toString()方法(Date对象除外,Date对象会调用valueOf()方法)。
回到我们的问题。
eval()的问题
首先我们看eval()的问题。JS里是没有块作用域的,但是JS支持语句块,类似于:
var foo=1,bar=2;{foo;bar} // 2
更特殊的,JS还支持标签语句(给 break 和 continue 用):
hehe : {
loop: for(;;){
if(t() == 42) break hehe;
}
}
正如你所见,标签语句 不要求 修饰循环语句,你可给任意语句加上标签。因此,下面的代码
{a: 1}
不仅符合 JavaScript,更可以解释成:一个语句块,里面一条标签语句,标签是 a,内容是 1。然而这段代码又符合对象直接量的语法(一个对象,属性 a,值为 1),歧义就这样产生了。
可以看出,如果不加上某些限制的话,光靠 JavaScript 语法就无法阐述语句「{a: 1}」到底是一个语句块,还是一个对象直接量。为了解决这个问题,ECMA 的方法十分简单粗暴:在语法解析的时候,如果一个语句以「{」开头,就只把它解释成语句块。换用形式语法的说法,就是「表达式语句不能以『{』开头」。对表达式语句开头的另一个限制——限制「function」出现在开头——同理。(可参考:JavaScript原理:其二)
这样的话,eval在执行eval('{a:1}')的时候,它会认为a是标签,而1是内容,而在执行eval('{"a":1}')的时候,因为此时的"a"是字符串了,所以会认为其是一个语句块,语句块内部"a"是字符串,1是数值,"a":1(字符串:数字)明显是会报错的:
"a":1
SyntaxError: Unexpected token :
从而会报错处理。此时的解决办法就是众所周知的加上()强制运算符,强制括号内的内容作为整体的一个表达式返回,形成最终合法的对象。
拨开云雾见天日
知道了这个,那么一切就很清楚了。在执行eval('{}+[]')的时候,首先执行前面的{}按照语句块处理,按语句块处理的结果也就相当于你在编译器里这么写代码:
{a:2}
+1
onsole.error({a:2}+1); // "{object Object}1"
而eval('{a:2}+1')也就相当于上面代码的前两行。那么执行eval('{a:2}1')也会返回1.那么eval('{}+[]')其实也就相当于于执行eval('+[]'),也就相当于调用Number([]),而将一个数组转化为数字的时候,上面在解释toString和valueOf的时候有所提及,会先调用其toString()方法,而将一个数组调用toString()方法相当于调用其join(',')方法,[].toString()结果为空字符串:""。+""为0.
[].toString(); // ""
[1,2,3].toString() === '1,2,3'; // true
+[] === +"" // true,为0
+[1,2,3] // NaN
所以空数组最终返回的结果是0.从而eval('{}+[]')结果为0.而eval('{}+[1,2,3]')的结果为NaN。而eval('[]+{}')则会认为{}是作为运算的一部分参加的,会调用二者的toString()后再计算,结果自然为:"{object Object}"。
但是,其实真正的执行{}+[]的时候,都会调用它们的toString()方法。{}调用toString()后为"{object Object}",而[].toString为"",所以最终结果为:"{object Object}"。
结论
Chrome的控制台其实并不靠谱。这只是他不靠谱的一个体现,还有一个体现就是当我们在打印一个对象在调试的时候,我们想打印修改前的操作结果,但是打印的很可能就是已经修改后的结果了。类似于下面这样:
var mm = {a:1};
setTimeout(function(){
console.error(mm.a);// 2
});
mm.a = 2;
所以,不要太相信Chrome的控制台,有时打断点是必要的。