文章

隐式转换与运算符陷阱:+ 号、比较运算、空值

面试必问的隐式转换输出题:+ 号何时拼接何时相加、关系比较的字典序陷阱、 0 和空字符串怎么判断——以及 ?? 和 || 到底怎么选。

隐式转换与运算符陷阱:+ 号、比较运算、空值

一句话概括

如果说类型转换是”JS 主动帮你换类型”,那隐式转换就是”你根本没让它换,它自己换了”——最常见于运算符:+ 号、- * / %、比较运算符(> < >= <=)、&& / ||。面试官爱出这类输出题,因为规则就三条但每条都有反直觉的坑:+ 号”见到字符串就拼接”、比较运算符”两边都是字符串才按字典序”、以及最危险的空值陷阱(null / undefined / 0 / "" 在判断和默认值场景下互相”伪装”)。这三条规则记牢,业务代码里一半的隐性 bug 都能提前避开。

核心知识点

1. + 号:唯一”既能加又能拼”的运算符

二元 + 的规则只有一句:先把两边转成原始值,只要有一边是字符串,就做字符串拼接;否则做数字加法。这是全 JS 最容易被背刺的运算符:

1
2
3
4
5
6
7
1 + "2";        // "12"    ← 有字符串,拼接
"5" + true;     // "5true" ← true 被转成 "true"
[] + [];        // ""      ← 两边都转成 "",拼接成空串
[] + {};        // "[object Object]" ← 空数组 "" + 对象字符串
[1, 2] + [3, 4] // "1,23,4" ← 不是 [4,6]!数组先转字符串再拼接
null + 1;       // 1       ← null 转数字是 0,没有字符串,走加法
undefined + 1;  // NaN     ← undefined 转数字是 NaN

再看一个必考名场面——{} + [] 的两种答案:

1
2
3
4
5
// 在语句开头:{} 被当成代码块(Block),整句等价于 +[] → 0
{} + [];          // 0

// 在表达式里(如 console.log 参数):{} 是对象字面量 → 字符串拼接
console.log({} + []);   // "[object Object]"

一元 + 是完全不同的东西:它只做一件事——转数字(+"42" → 42、+true → 1)。它是 Number() 的简写,但可读性差,不推荐用在业务代码里。

2. 减乘除取模:一律转数字

-、*、/、% 没有拼接模式,永远把两边转成数字再运算。这也是”字符串减数字”合法的原因:

1
2
3
4
5
6
7
"6" - 1;    // 5   ← 字符串转数字
"6" * "3";  // 18
"abc" - 1;  // NaN ← 转不了数字就是 NaN
"12px" - 0; // NaN ← 注意:Number("12px") 也是 NaN

// 为什么 "5" + 2 是 "52" 而 "5" * 2 是 10?
// 因为 + 有拼接模式,- * / % 只有数字模式

和 parseInt 的区别是高频追问:Number("12px") 是 NaN(整个串都要是数字),而 parseInt("12px") 是 12(从头解析到第一个非法字符)。所以解析”12px”这种带单位的字符串只能用 parseInt / parseFloat。

3. 关系比较:两边都是字符串才按字典序

> < >= <= 的规则:如果两边都是字符串,按字典序(Unicode 码点)比较;否则一律转数字比较。陷阱都藏在”一个字符串一个数字”和”看似字典序实则数字”里:

1
2
3
4
5
6
7
8
9
"10" > 9;        // true  ← 字符串转数字 10 > 9,不是字典序!
"10" > "9";      // false ← 两边都是字符串,按字典序:"1" < "9"
"abc" < "abd";   // true  ← 字典序逐字符比较
"a" > 1;         // false ← "a" 转数字是 NaN,任何比较都是 false

// 真实 bug:日期字符串比较
"2026-10-02" > "2026-10-01"   // true  ← YYYY-MM-DD 格式恰好字典序=时间序,能比
"2026/10/02" > "2026/10/01"   // true  ← 也成立,因为格式统一
"10:02" > "9:59"              // false ← 字典序!"1" < "9",时间比较翻车

安全做法:比较时间转成时间戳(Date.parse 或 getTime()),比较数字先用 Number() 显式转好再比,绝不依赖隐式转换。

4. 空值四兄弟:null / undefined / 0 / ““(业务 bug 重灾区)

这四个值在布尔判断和相等比较里行为完全不同,是线上 bug 最常见的来源:

1
2
3
4
5
6
7
8
9
10
11
12
// 布尔判断(if / && / ||):只有 null / undefined / 0 / "" 都是假
if (0) { }        // 不执行
if ("") { }       // 不执行
if (null) { }     // 不执行
if (undefined) { }// 不执行

// 宽松相等:它们并不互相"等价"!
null == undefined;  // true  ← 唯一互相等价的
null == 0;          // false ← 出乎意料
undefined == 0;     // false
"" == 0;            // true  ← 空串转数字是 0
"" == null;         // false ← 又一个出乎意料

最危险的业务场景:接口返回”可选数字”字段,用户确实传了 0:

1
2
3
4
5
6
7
8
9
10
// ❌ 0 是合法值却被吞掉:count 为 0 时走默认值
const count = res.count || 10;        // res.count === 0 → 10(错误!)

// ❌ 更隐蔽:判断"有没有填"时 0 被当没填
if (!res.count) { return "未填写"; }  // 填了 0 也被当成未填写

// ✅ 显式判断:undefined / null 才算没填
if (res.count === undefined || res.count === null) { return "未填写"; }
// ✅ 或利用 == null 的合法用法(只覆盖 null 和 undefined)
if (res.count == null) { return "未填写"; }

5. ?? 与 ||:0 和 “” 是合法值时千万别用 ||

|| 取”第一个真值“(遇到 0 / "" / false 就跳过),?? 取”第一个非 null/undefined 的值”(0 / "" / false 都算有效值)。这是 ES2020 引入 ?? 的原因:

1
2
3
4
5
6
7
8
9
10
11
12
// 场景:下拉框选项,value 为 0 代表"全部"
const value = 0;

const a = value || "默认";   // "默认"  ← 0 被吞,错误
const b = value ?? "默认";   // 0       ← 正确保留

// 场景:空字符串是合法输入(搜索关键词可以为空)
const kw = "";
const c = kw || "无关键词";  // "无关键词" ← 空串被吞
const d = kw ?? "无关键词";  // ""         ← 保留

// 记忆:|| 管"假值",?? 只管"空值(null/undefined)"

另一个配套考点:&& / || 返回的是操作数本身而不是布尔值,所以 a || b 拿到的可能是数字/字符串——这也是很多人误以为 || 返回 true/false 的原因。

其实你每天都在用

  • 金额计算:input.value 是字符串,total + input.value 变成字符串拼接,页面显示 “10020” 而不是 120——先 Number() 再算。
  • 时间区间比较:两个 HH:mm 字符串比大小,"9:00" > "10:00" 因为字典序返回 true,前端判断”开始早于结束”失灵。
  • 搜索框默认值:用户清空关键词后 kw || "全部" 把空字符串当”没填”,想保留空搜索必须用 ??。
  • 接口数字字段:后端返回 0 表示”无限制”,res.limit || 100 永远拿不到 0。
  • CSS 前缀拼接:style.width = 10 + "px" 是有意的隐式转换(字符串拼接),这种用法是合理的。
  • 埋点数据:data[key] + "" 统一转字符串是老代码里的常见写法,等价于 String(data[key])。

常见误解(FAQ)

❌ 误区1:”'6' - 1 能算出 5,那 '6' + 1 应该也是 7。” + 是唯一有字符串拼接模式的运算符,见到字符串就拼接("61");- * / % 永远转数字。记住”只有加号会拼串”这一条,输出题就对一半。

❌ 误区2:”'10' > '9' 是 true,因为 10 比 9 大。” 两边都是字符串时不转数字,按字典序比较:'1' 比 '9' 小,所以是 false。字符串比较永远先问”两边都是字符串吗”。

❌ 误区3:”null、undefined、0、'' 在 == 下都相等。” 只有 null == undefined 是 true。null == 0、null == ""、undefined == 0 全是 false,而 "" == 0 又是 true(空串转数字是 0)。这四个值的相等关系必须逐个记。

❌ 误区4:”|| 返回布尔值 true/false。” || 返回的是操作数本身。null || "默认" 返回字符串 "默认",0 || 1 返回 1。它只在布尔上下文(if 里)才会被当成真假判断。

❌ 误区5:”?? 和 || 可以随便替换。” 不能。?? 只排除 null/undefined,|| 排除所有假值。0 ?? 1 是 0,0 || 1 是 1——当 0、”“、false 是合法业务值时,用 ??。另外 ?? 不能和 && / || 混用(a || b ?? c 会语法报错,要加括号)。

一句话总结

隐式转换的坑全在运算符上:+ 号有字符串就拼接、- * / % 永远转数字、关系比较两边都是字符串才走字典序、0/"" 在 || 和布尔判断里会被当”没有”——业务上判断”是否为空”用 == null,默认值要保留 0 或空串就用 ??。

本文由作者按照 CC BY 4.0 进行授权