JavaScript 相等比较

JavaScript 中 `==`、`===` 和 `Object.is` 的差异,相等比较算法,以及隐式类型转换带来的常见误解。

#resource / javascript #resource / ecmascript #type / concept #status / evergreen

[!info] related notes

JavaScript 相等比较

一句话定义

JavaScript 有三种相等比较方式:==(宽松相等,会做类型转换)、===(严格相等,不做类型转换)和 Object.is(同值相等,处理 NaN 和 -0 更精确)。

三种比较方式总览

比较方式类型转换NaN 比较+0 vs -0推荐场景
==falsetrue仅用于 null/undefined 判断
===不会falsetrue大多数场景
Object.is不会truefalse特殊数值语义

===:严格相等

规则

  • 类型不同,直接 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:nullundefined 只彼此相等

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 == NaNNaN === 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:有意设置为空

它们都表达”空”,所以宽松比较下把它们看成一类。

但又为了防止太离谱的隐式数值比较,没有让它们去等于 0false

null == 0     // false
undefined == 0 // false

这说明设计者想表达的是:

“它们彼此接近,但不是普通的数值 0,也不是布尔 false。“

实践建议

1. 绝大多数场景优先用 ===

因为它最稳定、最可预测。

if (x === 0) {}
if (x === null) {}

2. 唯一常见可以接受 == 的场景

就是同时判断 nullundefined

if (x == null) {
  // x 是 null 或 undefined
}

因为这是 == 里少数非常有用且可控的设计。

3. 判断 NaNNumber.isNaN

Number.isNaN(x)

不要写:

x === NaN

4. 不要依赖奇技淫巧式隐式转换

例如别写:

if (arr == false) ...

而要明确写:

if (arr.length === 0) ...

连续比较与连续相等

连续写 a == b == ca < b < c 时,JS 不会按数学直觉理解,而是从左到右逐个求值,中间结果(布尔值)会参与下一步隐式转换。

规则

a == b == c 等价于 (a == b) == c

a < b < c 等价于 (a < b) < c

每一步的结果都是布尔值,下一步比较时布尔值会按 true -> 1false -> 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) == 20 == 2false
2 == 1 == 0(false) == 00 == 0true
0 < 1 < 2(true) < 21 < 2true
1 < 2 < 3(true) < 31 < 3true
2 > 1 > 0(true) > 01 > 0true
3 > 2 > 1(true) > 11 > 1false

安全写法

不要依赖连续比较,用 && 拆开:

// ❌ 有歧义
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:会触发哪种抽象操作?

  • ToBoolean
  • ToNumber
  • ToPrimitive

再问 4:对象转 primitive 会得到什么?

  • 数组通常像字符串:[] -> ''[1] -> '1'
  • 普通对象通常:'[object Object]'

一旦这么拆,90% 的诡异题都能推出来。

信息参考

面试要点

来自 equality-operators-interview-question 的面试视角整理。

一句话回答

== 会先做隐式类型转换再比较,=== 不做隐式类型转换,要求类型和值都相同。

最稳的回答主线

  • == 更宽松,但规则复杂
  • === 更严格,也更可预测
  • 工程里大多数场景优先 ===

面试里补一句就够了

比如 1 == '1'true,但 1 === '1'false,这就说明 == 会带来类型转换成本。

最短记忆方式

== 会转类型,=== 不会。

创建于 2026/3/19 更新于 2026/5/27