前言
这两天都在看神三元大佬的总结的JS技术博客,在(建议收藏)原生JS灵魂之问, 请问你能接得住几个?(上)中发现了一个有趣的现象,就是[] == []
返回的是false
,而[] == ![]
返回的却是true
,我就记录下来了,万一面试就遇上了呢😎😎
问题重现
让我们先看看这个令人困惑的表达式:
console.log([] == ![]); // 输出: true
初看之下,这似乎完全不合理:一个空数组怎么会等于它的非值呢?要理解这一点,我们需要深入JavaScript的类型转换规则。
理解 == 操作符
JavaScript中的==
(宽松相等)操作符会在比较前执行类型转换,这与===
(严格相等)操作符不同。当比较的两个值类型不同时,==
会尝试将它们转换为相同类型后再比较,而对于===
来说,必须要数据和类型都一样才会返回true。
== 的类型转换规则
-
如果类型相同,直接比较值
-
如果类型不同:
(1) null 和 undefined
null
和undefined
在==
比较时相等- 它们与其他任何值(包括
0
、false
、''
)比较时都不相等
(2) 字符串和数字比较
-
字符串会尝试转换为数字
-
转换规则:
- 空字符串
''
转换为0
- 纯数字字符串(如
'123'
)转换为对应数字(123
) - 非数字字符串(如
'abc'
)转换为NaN
- 包含数字但非纯数字的字符串(如
'123abc'
)也转换为NaN
- 空字符串
(3) 布尔值与其他类型比较
- 布尔值会先转换为数字(
true
→1
,false
→0
) - 转换后再按规则与其他值进行比较
(4) 对象与原始值比较
-
对象会尝试通过
valueOf()
和toString()
方法转换为原始值 -
转换顺序:
- 先调用对象的
valueOf()
方法 - 如果返回的不是原始值,再调用
toString()
方法 - 如果仍然不是原始值,则抛出
TypeError
- 先调用对象的
-
日期对象特殊:优先调用
toString()
而非valueOf()
(5) 其他特殊情况
NaN
与任何值(包括自身)比较都返回false
- 数字
+0
和-0
在==
比较时被视为相等
分解 [] == ![] 的求值过程
让我们一步步拆解这个表达式:
-
计算![] :
[]
是一个对象(数组),在布尔上下文中所有对象都是true
![]
即!true
,结果为false
现在表达式变为:
[] == false
-
应用 == 规则:
- 一边是对象(
[]
),一边是布尔值(false
) - 根据规则,布尔值先转为数字:
false
→0
- 现在表达式变为:
[] == 0
- 一边是对象(
-
对象转为原始值:
- 对象在与原始值比较时,会尝试转为原始值
- 这个过程首先调用
valueOf()
,如果返回的不是原始值,再调用toString()
- 对于数组,
valueOf()
返回数组本身(不是原始值),所以调用toString()
[].toString()
返回空字符串""
- 现在表达式变为:
"" == 0
-
字符串与数字比较:
- 字符串转为数字:
""
→0
- 最终比较:
0 == 0
,结果为true
- 字符串转为数字:
JavaScript 的对象到原始值转换
理解对象如何转为原始值是解决这类问题的关键。JavaScript在对象到原始值的转换中遵循以下规则:
转换步骤
-
如果对象有
[Symbol.toPrimitive]
方法(一般对象都有内置的这个方法),调用它 -
否则,如果期望类型是"string"或默认:
- 先调用
toString()
,如果返回原始值则使用 - 否则调用
valueOf()
,如果返回原始值则使用
- 先调用
-
如果期望类型是"number":
- 先调用
valueOf()
,如果返回原始值则使用 - 否则调用
toString()
,如果返回原始值则使用
- 先调用
-
如果以上都没有返回原始值,抛出TypeError
var obj = {
value: 3,
valueOf() {
return 4;
},
toString() {
return '5'
},
[Symbol.toPrimitive]() {
return 6
}
}
console.log(obj + 1); // 输出7
这段代码定义了一个对象 obj
,它重写了 valueOf()
、toString()
和[Symbol.toPrimitive]()
方法。当对象参与加法运算时,JavaScript 会优先调用 [Symbol.toPrimitive]()
方法获取原始值(返回 6),因此 obj + 1
的结果是 6 + 1 = 7
。如果没有 Symbol.toPrimitive
,则会依次尝试 valueOf()
和 toString()
。如果不了解Symbol.toPrimitive
方法的可以看Symbol.toPrimitive。
如何让if(a == 1 && a == 2)返回true??
其实就是用了上面那个方法的应用
var a = {
value: 0;
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2) // true
这段代码定义了一个对象 a
,它有一个 value
属性初始为 0,并重写了 valueOf
方法,使得每次调用时 value
自增 1 并返回新值。当执行 a == 1 && a == 2
时,由于 ==
会触发 valueOf
方法,第一次比较 a == 1
返回 true
(value
从 0 变为 1),第二次比较 a == 2
也返回 true
(value
从 1 变为 2),因此最终结果为 true
。
常见对象的转换结果
-
数组:
[].toString()
→""
[1, 2, 3].toString()
→"1,2,3"
[null].toString()
→""
[undefined].toString()
→""
-
对象:
({}).toString()
→"[object Object]"
-
日期:
new Date().toString()
→ 当前日期时间的字符串表示
更多令人困惑的例子
理解了这些规则后,我们可以解释更多看似奇怪的现象:
console.log([] == 0); // true ([]→""→0)
console.log([1] == 1); // true ([1]→"1"→1)
console.log([1,2] == "1,2"); // true
console.log({} == "[object Object]"); // true
console.log([] == []); // false (两个不同对象)
console.log({} == {}); // false (两个不同对象)
大家可能会比较疑惑为什么[] == []
和{} == {}
返回的都是false,让我们继续往下看吧
虽然两个空数组 []
看起来相同,但它们在内存中是两个不同的对象,拥有不同的引用地址。==
比较时不会递归比较数组的内容,而是直接比较引用地址,因此返回 false
。
-
引用类型比较的是内存地址,而不是内容。
-
每次声明
[]
或{}
都会创建一个新的对象,即使内容相同,引用也不同。 -
如果要比较数组或对象的内容,需要手动遍历或使用工具函数(如 JSON.stringify() 或 Lodash 的
_.isEqual
):console.log(JSON.stringify([]) === JSON.stringify([])); // true(比较字符串化的内容) console.log(JSON.stringify({}) === JSON.stringify({})); // true
补充说明
-
如果变量指向同一个对象,比较会返回
true
:const arr = []; console.log(arr == arr); // true(同一引用)
注意事项和最佳实践
-
避免使用 ==:
- 由于这些复杂的转换规则,大多数情况下应该使用
===
来避免意外行为 - ESLint等工具可以配置为禁止使用
==
- 由于这些复杂的转换规则,大多数情况下应该使用
-
明确转换:
-
如果需要类型转换,最好显式地进行:
Number([]); // 0 String([]); // "" Boolean([]); // true
-
-
自定义对象转换:
-
可以为自定义对象实现
[Symbol.toPrimitive]
、valueOf()
或toString()
来控制转换行为const obj = { valueOf() { return 123; }, toString() { return "hello"; }, [Symbol.toPrimitive](hint) { return hint === 'string' ? this.toString() : this.valueOf(); } };
-
-
注意 falsy 值:
-
JavaScript中只有以下值在布尔上下文中为
false
:false
0
""
null
undefined
NaN
-
所有对象(包括空数组、空对象)在布尔上下文中都是
true
-
为什么设计成这样?
JavaScript的这种设计源于其早期需要处理HTML中的松散类型:
- 网页中的值大多是字符串:需要灵活地与数字比较
- 向后兼容:早期行为被保留以避免破坏现有网站
- 灵活性:允许快速原型开发,但牺牲了严谨性
总结
[] == ![]
为true
的背后是JavaScript复杂的类型转换规则:
![]
先转为false
false
转为数字0
[]
通过toString()
转为空字符串""
""
转为数字0
- 最终比较
0 == 0
为true
理解这些规则有助于我们:
- 调试奇怪的比较结果
- 编写更健壮的代码
- 理解JavaScript的设计哲学
记住,当你不确定时,使用===
而不是==
,或者显式地进行类型转换,这样可以避免大多数意外行为。
思考题
试试解释以下表达式的值:
console.log([] + []); // ?
console.log([] + {}); // ?
console.log({} + []); // ?
console.log({} + {}); // ?
console.log([] - []); // ?
console.log({} - {}); // ?
答案将在评论区公布!(提示:考虑运算符重载和类型转换)