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、-0、NaN、null、undefined和false是假值; - 对象是否“为空”不会影响它是真值还是假值。
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
六、抽象相等比较的核心规则
下面是面向开发者的简化规则,不代替规范算法:
- 两个操作数类型相同:大致按严格相等的值规则比较;
NaN不等于自身,+0和-0相等; null只与undefined抽象相等,不会转换为0或空字符串;- 布尔值先转换为数字;
- 字符串和数字比较时,字符串转换为数字;
- 对象和原始值比较时,对象执行 ToPrimitive,可能调用自定义的
Symbol.toPrimitive、valueOf或toString; - 两个不同对象通常只有在引用同一对象时才抽象相等;
- BigInt 与数字的
==比较有专门规则,例如1n == 1为true、1n === 1为false;但+等算术运算不会自动混合 BigInt 和 Number,显式转 Number 还可能产生精度损失; - Symbol 不会被安全地隐式转换为字符串或数字,相关比较应显式处理。
console.log(1n == 1) // true
console.log(1n === 1) // false
try {
1n + 1
} catch (error) {
console.log(error.name) // TypeError
}
自定义 Symbol.toPrimitive 可以改变对象转换结果,因此“对象总是先 valueOf 再 toString”也只是普通对象的一种默认路径:
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 有一个常见但应明确的用途:同时匹配 null 和 undefined:
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
十、工程建议
- 默认使用
===,需要区分NaN或-0时使用Object.is; - 需要把字符串解析成数字、布尔值或日期时显式转换并校验结果;
- 不要用
Boolean(value)的结果推断对象是否为空; - 不要依赖数组/对象的隐式字符串化作为业务协议;
- 如果必须使用
==,在代码旁边写清楚希望保留的转换语义; - 处理用户输入时还要考虑
Symbol.toPrimitive、Proxy、getter 和异常,不能把类型转换看成无副作用操作。
十一、历史图示与来源
原文的相等比较图已下载到本地:

图片来源:原始图片。图示属于历史教学材料,具体判断应以 ECMAScript 抽象相等比较算法为准。
十二、总结
[] == ![]
→ [] == false
→ [] == 0
→ '' == 0
→ 0 == 0
→ true
({}) == !({})
→ ({}) == false
→ ({}) == 0
→ '[object Object]' == 0
→ NaN == 0
→ false
真正需要记住的不是这些奇怪结果,而是:! 使用 ToBoolean,== 可能执行 ToNumber 和 ToPrimitive;数组和对象都是真值;普通对象的 toString() 返回字符串,不是 NaN。理解规则后,在业务代码中尽量显式转换并使用严格比较。