引言
“为什么 [] + [] 结果是空字符串?”
“为什么 [] + {} 结果是 [object Object]?”
“为什么 {} + [] 在浏览器里结果是 0?等等,难道 JavaScript 还会看心情办事?”
我第一次遇到这些问题的时候,正坐在面试官的对面。他微笑着说:“小伙子,你猜猜 [] + ![] 的结果是什么?”我当时心想,这不是欺负老实人吗?结果我报了个答案,面试官摇了摇头,然后给我上了一课。
从那以后,我终于明白:JavaScript 的加法运算符,简直就是一场混乱的相亲大会——两个不同类型的值被撮合在一起,过程充满了眼泪和规则。
今天,我们就来扒一扒这个“+”运算符背后的秘密。
一、先看几个离谱的例子
打开浏览器控制台,输入以下代码,看看结果:
[] + [] // "" (空字符串)
[] + {} // "[object Object]"
{} + [] // 0 (什么鬼?)
{} + {} // NaN (更离谱了)
true + true // 2 (爱情让人变数学天才)
false + false // 0
false + true // 1
[] + ![] // "false" (哦?)
!![] + !![] // 2 (两个真值相加等于 2?)
是不是一脸懵逼?别急,我们一起来当侦探,揭开这层迷雾。
二、加法运算符的相亲规则
在 JavaScript 中,加法运算符 + 主要有两个作用:
- 数值加法(如果两边都是数值)
- 字符串拼接(如果至少有一边是字符串)
但问题来了:当两边都不是数值也不是字符串,或者类型不一样时,JavaScript 会启动一套复杂的隐式转换规则,就像给两个不同世界的人安排相亲。
2.1 第一步:先看有没有对象
如果任一操作数是对象(包括数组、函数、普通对象),JavaScript 会尝试把它转换成原始值(primitive)。这个过程叫 ToPrimitive。
ToPrimitive 大致规则:
- 如果对象有
Symbol.toPrimitive方法,调用它。 - 否则,如果是日期对象,优先调用
toString。 - 否则,优先调用
valueOf,如果没得到原始值,再调用toString。
数组和普通对象怎么转原始值?
- 数组的
valueOf返回数组本身(不是原始值),所以会接着调用toString。[].toString()结果是空字符串"";[1,2,3].toString()结果是"1,2,3"。 - 普通对象的
valueOf也返回对象本身,所以会调用toString,得到"[object Object]"。
2.2 第二步:原始值相加
经过 ToPrimitive 后,两边都变成了原始值(可能是字符串、数字、布尔值、null、undefined、Symbol 等)。然后按照以下顺序:
- 如果其中一个原始值是字符串,那么就把另一个也转成字符串,然后拼接。
- 否则,把两个都转成数字,然后相加。
如果转数字时出现 NaN,结果就是 NaN。
是不是很简单?好,我们拿几个例子来实战演习。
三、实战演习:案例拆解
3.1 [] + []
左边:[] 是数组,ToPrimitive 走起:valueOf 返回自身,不是原始值;toString() 返回 ""(空字符串)。所以左边变成 ""。
右边同理,也是 ""。
现在两边都是字符串 ""。根据规则,有一个是字符串,执行字符串拼接:"" + "" = ""。
所以结果是空字符串。
3.2 [] + {}
左边:[] → ""(同上)。
右边:{} 是普通对象,valueOf 返回自身,toString() 返回 "[object Object]"。
两边变成:"" 和 "[object Object]"。至少一边是字符串,执行拼接:"" + "[object Object]" = "[object Object]"。
没问题。
3.3 {} + []
这个就有趣了。在浏览器控制台里输入 {} + [],你可能会得到 0。为什么?难道规则变了吗?
其实,这里的 {} 被解析成了一个代码块(block),而不是空对象。JavaScript 引擎把这一行理解成:一个空的代码块,后面跟着 + []。而 + [] 是一元正号运算符,它会把数组转成数字:[] 先转成空字符串 "",然后空字符串转数字是 0。所以结果是 0。
如果你这样写:({}) + [],加上括号,那么 {} 就被解析成对象,结果就是 "[object Object]"。
所以,{} + [] 的怪异结果是语法解析的锅,不是类型转换的错。
3.4 {} + {}
同理,第一个 {} 被当成代码块,后面是 + {}。+ {} 把对象转数字:{} ToPrimitive 得到 "[object Object]",然后 + "[object Object]" 转数字,得到 NaN。
所以结果是 NaN。
3.5 true + true
布尔值 true 转数字是 1,所以 1 + 1 = 2。
3.6 [] + ![]
先算右边:![],数组是对象,对象转布尔值是 true(因为所有对象都是真值),所以 ![] = false。
左边 [] 转成 ""。
现在两边是 "" 和 false。至少一边是字符串(""),所以把 false 转成字符串 "false",拼接:"" + "false" = "false"。
3.7 !![] + !![]
!![] 就是两次取反:![] 是 false,再 !false 是 true。所以两个 true 相加,数字 1 + 1 = 2。
四、还有更奇葩的吗?加法的“左右为难”
除了对象,还有 null 和 undefined 也要注意:
null转数字是0,转字符串是"null"。undefined转数字是NaN,转字符串是"undefined"。
看看这个:
null + 1 // 1 (null 转数字 0)
undefined + 1 // NaN
null + "hello" // "nullhello"
undefined + "hi" // "undefinedhi"
还有更骚的:
1 + 2 + "3" // "33" (从左到右:1+2=3,然后 3+"3"="33")
"1" + 2 + 3 // "123" (字符串拼接优先级高)
五、如何避免这些坑?
- 尽量使用显式转换,比如
Number()、String()、Boolean(),或者直接用模板字符串${}。 - 用
===代替==,避免隐式类型转换带来的意外。 - 对数组和对象进行运算前,想清楚它们会转成什么。比如你想拼接数组元素,可以用
join();想数字相加,先把它们转成数字。 - 括号优先,不要让
{}被误当成代码块。
六、总结:+ 运算符是面镜子,照出了 JavaScript 的隐式转换本质
加法运算符就像一场混乱的相亲,各种类型被强行拉郎配,结果往往出人意料。但只要你掌握了 ToPrimitive 和两阶段的转换规则(对象→原始值,原始值→数字/字符串),就能预测出绝大部分结果。
下次面试官再问你 [] + ![] 等于什么,你可以微微一笑:“等于 "false",因为左边数组变空串,右边布尔取反变 false 再变字符串,然后拼接。”
然后你还可以反问他:“那 {} + [] 等于多少呢?”看他是不是掉进代码块的坑里。
每日一问:你知道 !![] 为什么是 true 吗?评论区说说你的理解,顺便考考你的小伙伴!