JavaScript 相等比较
JavaScript 中 `==`、`===` 和 `Object.is` 的差异,相等比较算法,以及隐式类型转换带来的常见误解。
[!info] related notes
JavaScript 相等比较
一句话定义
JavaScript 有三种相等比较方式:==(宽松相等,会做类型转换)、===(严格相等,不做类型转换)和 Object.is(同值相等,处理 NaN 和 -0 更精确)。
三种比较方式总览
| 比较方式 | 类型转换 | NaN 比较 | +0 vs -0 | 推荐场景 |
|---|---|---|---|---|
== | 会 | false | true | 仅用于 null/undefined 判断 |
=== | 不会 | false | true | 大多数场景 |
Object.is | 不会 | true | false | 特殊数值语义 |
===:严格相等
规则
- 类型不同,直接
false - 类型相同,再按该类型的规则比较
示例
1 === '1' // false
null === undefined // false
细节
对于 number:
NaN === NaN // false
0 === -0 // true
注意这两个点后面会专门讲。
==:宽松相等
这是最复杂的,因为它允许跨类型比较。
但它不是“随便比”,而是有一整套固定规则。
你可以把它理解成:
先看类型组合,再按规范指定的方向做转换,最后比较。
最重要的几条规则
规则 A:类型相同,基本按 === 的结果来
1 == 1 // true
'1' == '1' // true
但:
NaN == NaN // false
因为 number 的相等规则里,NaN 和任何值都不相等,包括自己。
规则 B:null 和 undefined 只彼此相等
null == undefined // true
null == null // true
undefined == undefined // true
但:
null == 0 // false
null == false // false
undefined == 0 // false
这是一个非常特意的设计:
null/undefined在==中被当作”空值族”,只互相兼容,不随意参与数值比较。
规则 C:字符串和数字比较时,字符串转数字
'123' == 123 // true
'' == 0 // true
' \t\n' == 0 // true
因为:
Number('') // 0
Number(' \t\n') // 0
规则 D:布尔值和其他比较时,布尔先转数字
true == 1 // true
true == 2 // false
false == 0 // true
false == '' // true
最后一个别惊讶,它是分两步:
false == ''
=> 0 == ''
=> 0 == 0
=> true
规则 E:对象和 primitive 比较时,对象先 ToPrimitive
这是 [] == 0、[] == ''、{} == '[object Object]' 之类的来源。
比如:
[] == ''
过程:
[] -> ToPrimitive -> ''
'' == ''
=> true
再看:
[] == 0
过程:
[] -> ''
'' == 0
0 == 0
=> true
Object.is:更”精确”的同值比较
Object.is(NaN, NaN) // true
Object.is(0, -0) // false
它和 === 的主要区别就是这两个特殊点:
NaN+0 / -0
所以常记为:
===:工程里最常用Object.is:处理极端数值语义时更精确
经典案例完整拆解
1. NaN == NaN 和 NaN === NaN
NaN == NaN // false
NaN === NaN // false
原因:
NaN是 number- number 相等规则规定:
NaN不等于任何值,包括自己
如何判断 NaN:
Object.is(NaN, NaN) // true
Number.isNaN(x) // 推荐
x !== x // 只有 NaN 才为 true
2. null === undefined
null === undefined // false
原因: 类型不同。
3. null == undefined
null == undefined // true
原因: == 对空值族的特判。
4. [] == false
[] == false // true
推导:
false -> 0
[] -> ''
'' -> 0
0 == 0
=> true
5. [] == ![]
[] == ![] // true
推导:
![] => false(因为 [] 是 truthy)
[] == false
=> 同上,true
6. { } == []
{} == [] // false
原因:
两边都是对象,比较引用,不是内容,不是 primitive 结果。
为什么对象比较不用”内容相等”?
因为对象本质上是引用。语言内部通常把它们当成”引用 / identity-bearing value”。两个对象相等,不是看”长得像不像”,而是看:是不是同一个对象实例。
const a = {}
const b = a
a === b // true(同一个引用)
[] == [] // false(不同引用)
{} == {} // false(不同引用)
为什么不默认做深比较?
- 成本高
- 有循环引用
- 语义复杂(属性顺序、原型链、不可枚举属性、getter/setter)
- 会让
===变得昂贵且不可预测
所以语言把”引用相等”和”结构相等”分开。
7. [0] == 0
[0] == 0 // true
推导:
[0] -> '0'
'0' -> 0
0 == 0
=> true
8. '' == 0
'' == 0 // true
推导:
'' -> 0
0 == 0
=> true
为什么 null === undefined 是 false,但 null == undefined 是 true?
1. === 看类型
null === undefined // false
因为它们类型不同。
null是一个独立类型undefined是另一个独立类型
2. == 里特判为空值族
null == undefined // true
这是语言特意规定的,不是通过数值转换得出来的。
可以理解成:
undefined:缺失,未定义null:有意设置为空
它们都表达”空”,所以宽松比较下把它们看成一类。
但又为了防止太离谱的隐式数值比较,没有让它们去等于 0 或 false:
null == 0 // false
undefined == 0 // false
这说明设计者想表达的是:
“它们彼此接近,但不是普通的数值 0,也不是布尔 false。“
实践建议
1. 绝大多数场景优先用 ===
因为它最稳定、最可预测。
if (x === 0) {}
if (x === null) {}
2. 唯一常见可以接受 == 的场景
就是同时判断 null 和 undefined:
if (x == null) {
// x 是 null 或 undefined
}
因为这是 == 里少数非常有用且可控的设计。
3. 判断 NaN 用 Number.isNaN
Number.isNaN(x)
不要写:
x === NaN
4. 不要依赖奇技淫巧式隐式转换
例如别写:
if (arr == false) ...
而要明确写:
if (arr.length === 0) ...
连续比较与连续相等
连续写 a == b == c 或 a < b < c 时,JS 不会按数学直觉理解,而是从左到右逐个求值,中间结果(布尔值)会参与下一步隐式转换。
规则
a == b == c 等价于 (a == b) == c
a < b < c 等价于 (a < b) < c
每一步的结果都是布尔值,下一步比较时布尔值会按 true -> 1、false -> 0 转成数字。
示例拆解
0 == 1 == 2
// => (0 == 1) == 2
// => false == 2
// => 0 == 2 (false -> 0)
// => false
2 == 1 == 0
// => (2 == 1) == 0
// => false == 0
// => 0 == 0
// => true
0 < 1 < 2
// => (0 < 1) < 2
// => true < 2
// => 1 < 2 (true -> 1)
// => true
3 > 2 > 1
// => (3 > 2) > 1
// => true > 1
// => 1 > 1
// => false
总结表
| 表达式 | 展开 | 结果 |
|---|---|---|
0 == 1 == 2 | (false) == 2 → 0 == 2 | false |
2 == 1 == 0 | (false) == 0 → 0 == 0 | true |
0 < 1 < 2 | (true) < 2 → 1 < 2 | true |
1 < 2 < 3 | (true) < 3 → 1 < 3 | true |
2 > 1 > 0 | (true) > 0 → 1 > 0 | true |
3 > 2 > 1 | (true) > 1 → 1 > 1 | false |
安全写法
不要依赖连续比较,用 && 拆开:
// ❌ 有歧义
a < b < c
a == b == c
// ✅ 语义明确
a < b && b < c
a === b && b === c
脑内推导图
以后看到奇怪表达式,可以按这个顺序想:
先问 1:这是什么运算符?
=====Object.is
再问 2:两边是什么类型?
- primitive vs primitive
- object vs primitive
- object vs object
再问 3:会触发哪种抽象操作?
ToBooleanToNumberToPrimitive
再问 4:对象转 primitive 会得到什么?
- 数组通常像字符串:
[] -> '',[1] -> '1' - 普通对象通常:
'[object Object]'
一旦这么拆,90% 的诡异题都能推出来。
信息参考
- MDN - Equality comparisons
- ECMAScript Language Specification - Abstract Equality Comparison
- ECMAScript Language Specification - Strict Equality Comparison
面试要点
来自 equality-operators-interview-question 的面试视角整理。
一句话回答
== 会先做隐式类型转换再比较,=== 不做隐式类型转换,要求类型和值都相同。
最稳的回答主线
==更宽松,但规则复杂===更严格,也更可预测- 工程里大多数场景优先
===
面试里补一句就够了
比如 1 == '1' 为 true,但 1 === '1' 为 false,这就说明 == 会带来类型转换成本。
最短记忆方式
== 会转类型,=== 不会。