文章

类型转换与相等性全解析:==、===、ToPrimitive

面试必考的类型转换输出题:== 与 === 的区别、对象转原始值的 ToPrimitive 机制、 8 个假值,从规范推导 [] == ![] 为什么是 true。

类型转换与相等性全解析:==、===、ToPrimitive

一句话概括

JavaScript 是弱类型语言,运算和比较时经常”偷偷”帮你转换类型——这就是类型转换。面试官爱问它,因为它是输出题重灾区([] == ![] 到底等于什么?)也是线上 bug 的常见源头(接口返回的字符串数字和本地数字比较永远不等)。你不需要背下所有诡异结果,只需要掌握三条主线:显式转换的坑(Number/String/Boolean)、对象转原始值的 ToPrimitive 机制、== 宽松相等的五条规则。掌握这三条,任何输出题都能一步步推出来,而不是靠记答案。

核心知识点

1. 三种显式转换的特殊值(最容易翻车)

面试第一问通常是”JS 有哪几种类型转换”——答案是三种:转布尔、转数字、转字符串。但真正考的是特殊值:

1
2
3
4
5
6
7
8
9
10
11
12
13
Number(null);        // 0        ← 记不住,最常考
Number(undefined);   // NaN
Number("");          // 0
Number("  12  ");    // 12       ← 会去掉首尾空格
Number("12px");      // NaN      ← 和 parseInt 不同,一个字符都不能错

Boolean("0");        // true     ← 非空字符串都是 true
Boolean([]);         // true     ← 空数组也是 true!
Boolean({});         // true     ← 空对象也是 true!

String(null);        // "null"
String(undefined);   // "undefined"
String([1, 2]);      // "1,2"    ← 数组转字符串用逗号拼接

记忆口诀:数字转换只有 4 个特殊值(null→0、”“→0、undefined→NaN、带空格数字→去空格),布尔转换只有 8 个假值,其余全是真。

2. 8 个假值(falsy),其余全是真值

布尔上下文(if、&&、||、!)里,只有 8 个值是假,记住它们就稳了:

假值说明
false布尔假
0、-0数字零
0nBigInt 零(ES2020 新增,容易漏)
""空字符串
null空引用
undefined未定义
NaN非数字
1
2
3
4
5
// ❌ 以为空数组是假
if ([]) { console.log("执行"); }   // 会执行![] 是真值

// ✅ 要判断"数组为空"得看长度
if (list.length === 0) { /* 真的空 */ }

面试高频追问:"0"、"false"、[]、{} 都是真值——因为它们是”非空字符串”或”对象”。

3. 对象转原始值:ToPrimitive 三步走

对象在需要变成原始值(数字/字符串)时,JS 会调用内部抽象操作 ToPrimitive,按 hint(期望类型)决定调用顺序:

1
2
hint = "number" 或 "default"(==、+ 号等)→ 先 valueOf(),不行再 toString()
hint = "string"  (模板字符串、String())  → 先 toString(),不行再 valueOf()

每一步:Symbol.toPrimitive 优先级最高,有就直接用它;普通对象 valueOf() 默认返回自身(不是原始值,被跳过),所以最终几乎都落到 toString():

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {};
obj.valueOf();  // {}           ← 返回自身,不是原始值,被忽略
obj.toString(); // "[object Object]"

[].toString();          // ""              ← 空数组
[1, 2].toString();      // "1,2"
({}).toString();        // "[object Object]"

// 面试名场面:怎么让 a == 1 && a == 2 && a == 3 成立?
// 利用 valueOf 每次比较都自增
const a = { value: 0, valueOf: () => ++a.value };
a == 1 && a == 2 && a == 3;   // true!

两个特例记一下:Date 的 default 按 string 处理(先 toString);Symbol 永远只能显式转,"" + Symbol() 会直接报 TypeError。

4. == 宽松相等的五条规则(面试核心)

== 会做类型转换,按优先级排五条,从上到下命中一条就结束:

  1. null == undefined → true(唯一的例外,它俩”等价但不等型”)
  2. 有一边是 boolean → 先把它转数字(true→1、false→0)
  3. 一边 string 一边 number → 字符串转数字
  4. 一边 object 一边 原始值 → 对象走 ToPrimitive(hint 为 default)
  5. 两边都是原始值且类型不同 → 按上述规则递归;对象 vs 对象 → 比较引用

用这套规则推导面试名场面 [] == ![]:

1
2
3
4
5
6
7
8
9
[] == ![]
// 1. 右边先算:![] 是真值取反 → false
[] == false
// 2. 规则 2:boolean 转数字 → false → 0
[] == 0
// 3. 规则 4:对象转原始值,[].toString() → ""(hint default 先 valueOf 失败再 toString)
"" == 0
// 4. 规则 3:字符串转数字 → Number("") → 0
0 == 0   // → true!

再验证几个经典结论:

1
2
3
4
null == undefined;   // true,且这是 == 唯一推荐用法
null == 0;           // false ← 别被 Number(null) === 0 迷惑,== 有专门规则
NaN == NaN;          // false ← NaN 和任何值都不等,包括自己
{} == {};            // false ← 对象比较的是引用,两个不同对象永远不等

5. === 与 NaN 判断:工程实践怎么选

=== 不转换类型,类型不同直接 false。工程上默认用 ===,唯一的例外是判断 null/undefined:

1
2
3
4
5
6
// ✅ 工程最佳实践:用 === 判断空值
if (value === null || value === undefined) { /* 处理 */ }

// ✅ 更优雅的写法:x == null 同时覆盖 null 和 undefined
// (这是 == 唯一被官方推荐的用法,Airbnb 规范也认可)
if (value == null) { /* 处理 */ }

NaN 的判断是独立考点:

1
2
3
4
5
6
7
8
9
// ❌ 错误:NaN 不等于任何东西,包括自己
if (result === NaN) { }        // 永远不成立

// ✅ 正确:Number.isNaN(不先做类型转换)
if (Number.isNaN(result)) { }  // 只对真正的 NaN 返回 true

// 对比:全局 isNaN 会先把值转数字,'abc' 也会被判为 NaN
isNaN('abc');              // true  ← 有坑
Number.isNaN('abc');       // false ← 推荐

其实你每天都在用

  • 表单校验:Number(input.value) 拿到的是 NaN 而不是数字,所以用 Number.isNaN 判断输入是否合法。
  • 接口返回的 "5"(字符串)和本地 count + 1(数字)用 == 比较能通过,换成 === 就挂——这是线上最常见的”改了 === 出 bug”现场。
  • JSON.parse 失败返回的是异常而不是 null,很多人误以为 parse 失败会得到 undefined。
  • if (list.length) 判断数组非空——length 是 0 时是假,非 0 是真,能省一行代码。
  • 用户输入 "0" 想表示”0 岁”,但 if (age) 把它当假值跳过了——所以判断”有没有填”要用 !== ""。
  • input.value 永远是字符串,input.value - 0 或 +input.value 是转数字的老写法(但可读性差,推荐 Number())。

常见误解(FAQ)

❌ 误区1:”== 和 === 只是”严格不严格”的区别,差不多。” 差很多:== 会做最多四层类型转换,[] == ![] 这种”看似荒谬”的结果全靠它。=== 类型不同直接 false,逻辑可预测。默认永远用 ===。

❌ 误区2:”Number(null) 是 0,所以 null == 0 也是 true。” 大坑!== 对 null/undefined 有专门规则:null == undefined 为 true,null == 0、null == "" 都是 false。显式转换和宽松相等是两套规则,别混。

❌ 误区3:”空数组 []、空对象 {} 是假值。” 恰恰相反,它们是真值。8 个假值里只有 false/0/-0/0n/""/null/undefined/NaN,其余全是真。判断”数组空不空”要看 length。

❌ 误区4:”判断 NaN 用 isNaN() 就行。” 全局 isNaN 会先把参数转数字,isNaN('abc') 返回 true(字符串被转成 NaN)。要用 Number.isNaN,它不做转换,只有真正的 NaN 才返回 true。

❌ 误区5:”a == 1 && a == 2 && a == 3 能成立是因为 JS 有 bug。” 不是 bug,是 ToPrimitive 的正常行为——valueOf 每次比较都会被调用,返回自增后的值。这也是面试官验证你”真懂对象转原始值”的经典考题。

一句话总结

类型转换就记三条:显式转换只有 4 个数字特殊值 + 8 个假值,对象转原始值靠 ToPrimitive(hint 决定 valueOf/toString 顺序),== 有”null/undefined 互等、布尔转数字、字符串转数字、对象转原始值”四条转换规则——默认用 ===,只有判断空值时用 == null。

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