为什么控制台打印{}+[]===[]+{}为false?

本文探讨了JavaScript中控制台打印`{}+[]===[]+{}`为何为false的问题。通过分析`toString()`和`valueOf()`方法的调用顺序,以及`eval()`函数的影响,揭示了其背后的逻辑。最终得出结论,这涉及到JavaScript中运算符优先级和类型转换的深入理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

事由

前几天有人在微博上发了一张图片(回微博找没找到了),说对于下面的运算较为困惑:
[]+{}=== {}+[]       // 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的控制台,有时打断点是必要的。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值