技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JS 中鲜为人知的问题:[] == ![] 为 true,而 ({}) == !({}) 为 false

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/45-JS中鲜为人知的问题: [] == ![]结果为true,而 {} == !{}却为false
本文目录13 个章节
  1. 一、先看结果
  2. 二、先计算 ![] 和 !{}
  3. 三、[] == ![] 为什么是 true
  4. 四、({}) == !({}) 为什么是 false
  5. 五、为什么对象字面量要加括号
  6. 六、抽象相等比较的核心规则
  7. 七、数组和普通对象的转换对比
  8. 八、==、=== 和 Object.is
  9. 九、其他容易混淆的例子
  10. 十、工程建议
  11. 十一、历史图示与来源
  12. 十二、总结
  13. 参考资料

JS 中鲜为人知的问题:[] == ![] 为 true,而 ({}) == !({}) 为 false

Category(分类): JavaScript Status: 已整理(2026)

本文保留原文通过 [] == ![] 和对象比较解释隐式类型转换的主线,但修正了“引用类型存放在堆、比较一定是地址”“对象直接调用 toString 就得到 NaN”等过度简化。注意:表达式中的对象字面量需要加括号,{} 在语句开头可能被解析成代码块。

原文作者:magic_xiang

一、先看结果

console.log([] == ![]) // true
console.log(({}) == !({})) // false
console.log([] == !{}) // true
console.log(({}) == ![]) // false

== 是抽象相等比较(Abstract Equality Comparison),会根据操作数类型执行一组规范定义的转换;=== 是严格相等比较,通常不会为了比较而进行这类隐式类型转换。理解规则有助于阅读旧代码,但业务代码通常应优先使用 ===Object.is 和显式转换。

二、先计算 ![]!{}

逻辑非 ! 的第一步是把操作数转换为布尔值(ToBoolean):

  • 所有普通对象,包括空数组和空对象,都是真值;
  • 空字符串、0-0NaNnullundefinedfalse 是假值;
  • 对象是否“为空”不会影响它是真值还是假值。
console.log(Boolean([])) // true
console.log(Boolean({})) // true
console.log(![]) // false
console.log(!{}) // false

因此:

[] == ![]       等价于  [] == false
({}) == !({})   等价于  ({}) == false

这里没有调用数组或对象的 valueOf/toString! 只进行 ToBoolean。

三、[] == ![] 为什么是 true

按照抽象相等比较的过程:

第 1 步:处理布尔值

[] == false

当一侧是布尔值时,布尔值先转换为数字:

[] == 0

第 2 步:把对象转换为原始值

数组是对象,和数字比较时需要执行 ToPrimitive。普通数组的 valueOf() 返回数组对象自身,不能提供原始值,于是继续调用 toString()

console.log([].valueOf() === []) // false:每个字面量都是不同数组
console.log([].toString()) // ''
console.log(String([])) // ''

所以比较变成:

'' == 0

第 3 步:字符串转数字

空字符串转换为数字 0

console.log(Number('')) // 0

最终过程是:

[] == ![]
→ [] == false
→ [] == 0
→ '' == 0
→ 0 == 0
→ true

这不是因为“空数组等于 false”,而是多次隐式转换恰好得到相同的数字。

四、({}) == !({}) 为什么是 false

第 1 步:处理逻辑非

普通对象是真值,所以:

({}) == !({})
→ ({}) == false
→ ({}) == 0

第 2 步:把普通对象转换为原始值

对于普通对象,默认的 valueOf() 返回对象本身,仍然不是原始值,于是继续调用 toString()

const object = {}

console.log(object.valueOf() === object) // true
console.log(object.toString()) // [object Object]
console.log(String(object)) // [object Object]

因此比较变成:

'[object Object]' == 0

第 3 步:字符串转换为数字

console.log(Number('[object Object]')) // NaN

于是最终是:

({}) == !({})
→ ({}) == false
→ ({}) == 0
→ '[object Object]' == 0
→ NaN == 0
→ false

原文说“{}.toString() 返回 NaN”是不准确的:{}.toString() 返回字符串 "[object Object]",再把这个字符串转换成数字时才得到 NaN

五、为什么对象字面量要加括号

在表达式语句开头,{} 可能被解析为一个空代码块,而不是对象字面量:

// 语句开头的 {} 可能被解析成代码块;用括号明确它是对象
console.log(({}) == !({})) // false

写成括号可以明确表达对象:

console.log(({}) == !({})) // false
console.log(({}) == ![]) // false

在变量初始化、函数参数或赋值右侧,解析通常已经明确:

const object = {}
console.log(object == !object) // false

六、抽象相等比较的核心规则

下面是面向开发者的简化规则,不代替规范算法:

  1. 两个操作数类型相同:大致按严格相等的值规则比较;NaN 不等于自身,+0-0 相等;
  2. null 只与 undefined 抽象相等,不会转换为 0 或空字符串;
  3. 布尔值先转换为数字;
  4. 字符串和数字比较时,字符串转换为数字;
  5. 对象和原始值比较时,对象执行 ToPrimitive,可能调用自定义的 Symbol.toPrimitivevalueOftoString
  6. 两个不同对象通常只有在引用同一对象时才抽象相等;
  7. BigInt 与数字的 == 比较有专门规则,例如 1n == 1true1n === 1false;但 + 等算术运算不会自动混合 BigInt 和 Number,显式转 Number 还可能产生精度损失;
  8. Symbol 不会被安全地隐式转换为字符串或数字,相关比较应显式处理。
console.log(1n == 1) // true
console.log(1n === 1) // false

try {
  1n + 1
} catch (error) {
  console.log(error.name) // TypeError
}

自定义 Symbol.toPrimitive 可以改变对象转换结果,因此“对象总是先 valueOftoString”也只是普通对象的一种默认路径:

const custom = {
  [Symbol.toPrimitive](hint) {
    console.log(hint)
    return 1
  }
}

console.log(custom == 1) // true:调用自定义转换

七、数组和普通对象的转换对比

console.log(String([])) // ''
console.log(Number([])) // 0
console.log(String([1])) // '1'
console.log(Number([1])) // 1
console.log(String([1, 2])) // '1,2'
console.log(Number([1, 2])) // NaN

console.log(String({})) // [object Object]
console.log(Number({})) // NaN

这些结果来自对象到原始值、字符串转换和数字转换的组合,不代表数组或对象本身具有这些“类型值”。

7.1 空数组不是“假值”

if ([]) {
  console.log('执行') // 执行
}

console.log([] === false) // false
console.log([] == false) // true:发生隐式转换

if (value) 使用 ToBoolean;value == false 使用抽象相等比较。二者不能混用推理。

八、=====Object.is

console.log(0 == false) // true
console.log(0 === false) // false

console.log('' == 0) // true
console.log('' === 0) // false

console.log(null == undefined) // true
console.log(null === undefined) // false

console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
console.log(Object.is(0, -0)) // false

一般业务代码优先使用严格相等:

const input = '0'

console.log(input === 0) // false,不发生隐式转换
console.log(Number(input) === 0) // true:显式表达转换意图

== null 有一个常见但应明确的用途:同时匹配 nullundefined

function isMissing(value) {
  return value == null
}

console.log(isMissing(null)) // true
console.log(isMissing(undefined)) // true
console.log(isMissing(0)) // false

如果团队规范不允许 ==,可以写成 value === null || value === undefined,语义更直观。

九、其他容易混淆的例子

console.log([] == 0) // true
console.log([0] == 0) // true
console.log([1] == 1) // true
console.log([1, 2] == '1,2') // true

console.log(false == '0') // true
console.log(false == []) // true
console.log(true == [1]) // true

console.log({} === {}) // false:两个不同对象
console.log([] === []) // false:两个不同数组

const same = {}
console.log(same === same) // true
console.log(same == same) // true

对象比较默认比较身份,不会深度比较结构:

console.log({ id: 1 } == { id: 1 }) // false
console.log({ id: 1 } === { id: 1 }) // false

十、工程建议

  1. 默认使用 ===,需要区分 NaN-0 时使用 Object.is
  2. 需要把字符串解析成数字、布尔值或日期时显式转换并校验结果;
  3. 不要用 Boolean(value) 的结果推断对象是否为空;
  4. 不要依赖数组/对象的隐式字符串化作为业务协议;
  5. 如果必须使用 ==,在代码旁边写清楚希望保留的转换语义;
  6. 处理用户输入时还要考虑 Symbol.toPrimitive、Proxy、getter 和异常,不能把类型转换看成无副作用操作。

十一、历史图示与来源

原文的相等比较图已下载到本地:

抽象相等比较历史图示

图片来源:原始图片。图示属于历史教学材料,具体判断应以 ECMAScript 抽象相等比较算法为准。

十二、总结

[] == ![]
→ [] == false
→ [] == 0
→ '' == 0
→ 0 == 0
→ true

({}) == !({})
→ ({}) == false
→ ({}) == 0
→ '[object Object]' == 0
→ NaN == 0
→ false

真正需要记住的不是这些奇怪结果,而是:! 使用 ToBoolean,== 可能执行 ToNumber 和 ToPrimitive;数组和对象都是真值;普通对象的 toString() 返回字符串,不是 NaN。理解规则后,在业务代码中尽量显式转换并使用严格比较。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS