当 JavaScript 试图做加法:一场混乱的“相亲”大会

引言

"为什么 [] + [] 结果是空字符串?"

"为什么 [] + {} 结果是 [object Object]?"

"为什么 {} + [] 在浏览器里结果是 0?等等,难道 JavaScript 还会看心情办事?"

我第一次遇到这些问题的时候,正坐在面试官的对面。他微笑着说:"小伙子,你猜猜 [] + ![] 的结果是什么?"我当时心想,这不是欺负老实人吗?结果我报了个答案,面试官摇了摇头,然后给我上了一课。

从那以后,我终于明白:JavaScript 的加法运算符,简直就是一场混乱的相亲大会------两个不同类型的值被撮合在一起,过程充满了眼泪和规则。

今天,我们就来扒一扒这个"+"运算符背后的秘密。

一、先看几个离谱的例子

打开浏览器控制台,输入以下代码,看看结果:

javascript 复制代码
[] + []           // ""  (空字符串)
[] + {}           // "[object Object]"
{} + []           // 0   (什么鬼?)
{} + {}           // NaN (更离谱了)
true + true       // 2   (爱情让人变数学天才)
false + false     // 0
false + true      // 1
[] + ![]          // "false"   (哦?)
!![] + !![]       // 2   (两个真值相加等于 2?)

是不是一脸懵逼?别急,我们一起来当侦探,揭开这层迷雾。

二、加法运算符的相亲规则

在 JavaScript 中,加法运算符 + 主要有两个作用:

  1. 数值加法(如果两边都是数值)
  2. 字符串拼接(如果至少有一边是字符串)

但问题来了:当两边都不是数值也不是字符串,或者类型不一样时,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,再 !falsetrue。所以两个 true 相加,数字 1 + 1 = 2

四、还有更奇葩的吗?加法的"左右为难"

除了对象,还有 nullundefined 也要注意:

  • null 转数字是 0,转字符串是 "null"
  • undefined 转数字是 NaN,转字符串是 "undefined"

看看这个:

javascript 复制代码
null + 1          // 1 (null 转数字 0)
undefined + 1     // NaN
null + "hello"    // "nullhello"
undefined + "hi"  // "undefinedhi"

还有更骚的:

javascript 复制代码
1 + 2 + "3"       // "33"  (从左到右:1+2=3,然后 3+"3"="33")
"1" + 2 + 3       // "123" (字符串拼接优先级高)

五、如何避免这些坑?

  1. 尽量使用显式转换 ,比如 Number()String()Boolean(),或者直接用模板字符串 ${}
  2. === 代替 ==,避免隐式类型转换带来的意外。
  3. 对数组和对象进行运算前,想清楚它们会转成什么 。比如你想拼接数组元素,可以用 join();想数字相加,先把它们转成数字。
  4. 括号优先 ,不要让 {} 被误当成代码块。

六、总结:+ 运算符是面镜子,照出了 JavaScript 的隐式转换本质

加法运算符就像一场混乱的相亲,各种类型被强行拉郎配,结果往往出人意料。但只要你掌握了 ToPrimitive 和两阶段的转换规则(对象→原始值,原始值→数字/字符串),就能预测出绝大部分结果。

下次面试官再问你 [] + ![] 等于什么,你可以微微一笑:"等于 "false",因为左边数组变空串,右边布尔取反变 false 再变字符串,然后拼接。"

然后你还可以反问他:"那 {} + [] 等于多少呢?"看他是不是掉进代码块的坑里。


每日一问 :你知道 !![] 为什么是 true 吗?评论区说说你的理解,顺便考考你的小伙伴!

相关推荐
毛骗导演1 小时前
OpenClaw 技能系统源码解析:一个 Markdown 文件是怎么变成 AI 的能力的
前端·架构
cxxcode1 小时前
Node.js 进程间通信(IPC)方式总结
前端
Mintopia1 小时前
Client Time 与 Server Time:分布式系统中的时间一致性与落地实践
前端·架构
ETA82 小时前
浏览器渲染机制与优化实战
前端·浏览器
柏箱2 小时前
文件上传漏洞入门:(upload-labs Pass-1 & Pass-2)
开发语言·前端·javascript
李剑一2 小时前
Cesium 海量点位不卡顿!图标动态聚合效果深度解析,看完直接抄代码!
前端·vue.js·cesium
CHU7290352 小时前
扭蛋机盲盒小程序:趣味交互与惊喜体验的功能设计
前端·小程序
CHU7290352 小时前
AI辅助工具小程序:多元功能助力,开启智能便捷新体验
前端·人工智能·小程序
予你@。2 小时前
Vue 项目中如何引用本地字体(完整指南)
前端·javascript·vue.js