当 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,再 !false 是 true。所以两个 true 相加,数字 1 + 1 = 2。

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

除了对象,还有 null 和 undefined 也要注意:

  • 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 吗?评论区说说你的理解,顺便考考你的小伙伴!

相关推荐
三小河15 分钟前
从 Markdown 到 Generative UI:AI 如何从“生成答案”进化到“生成界面”?
前端·人工智能·后端
请为小H留灯20 分钟前
Windows 本地前端项目一键运行指南:Volta+Node+pnpm + 阿里镜像完整初始化流程
前端·pnpm·node·js
J船长38 分钟前
从 App 开发到 Unity 游戏开发:建立认知地图,结合 AI 开始实践
前端
Highcharts.js1 小时前
用Highcharts开发动态主从图表|双图联动的时间序列图表示例
javascript·数据库·信息可视化·highcharts·图表开发·动态图表
牛奔1 小时前
mise 管理多版本 Go 项目
开发语言·前端·chrome·后端·golang
志尊宝1 小时前
Vue3 零基础每日笔记(076):防抖节流的正确使用——搜索框、按钮防重复提交、滚动监听
javascript·vue.js·笔记
能源革命2 小时前
Vue 网格拖拽布局组件全景指南:7 大方案深度对比
前端·javascript·vue.js
雷工笔记2 小时前
KingFusion 驾驶舱开发:ECharts小时模板实现动态时间窗口
javascript
知野小兔2 小时前
JavaScript中Map和Set详解
javascript