JS双等号比较符作用细节

都知道JS里”==“和”===“的区别,在于非同类型变量的比较中,”===“会直接返回false,而”==“则会先将两个比较值先转换为同一类型,再进行比较。然而,这里”先转换为同一类型“是什么样的规则呢?

一、容易忽视的比较细节

一直都没有在意”比较中的隐式类型转换“这个问题,因为常见的情况都太简单了:

"1" ==  1;            // true
1 == "1";             // true
0 == false;           // true

很简单, 很直观,直觉就是如此。直到我看见下面的比较:

![] == [] // true

看到这个比较前,我不知道未特殊处理(非劫持、代理等)的a值能使得 a == a && a == !a 会返回true,然而现在它就在这里:

> a = []
[]
> a == a && a == !a
true

是时候该彻底掌握”比较中的隐式类型转换“了。许多教程、书本都建议应该使用”===“,避免使用”==“,以避免代码中的不确定性以及”===“速度会更快(因为没有类型转换)。经典书籍《你不知道的Javascript》一书中的观点却非如此,我很赞成书中的观点,书中认为:存在”==“就应该搞清楚它的作用原理并且在代码中合理使用它,而不是一味避之。对我而言,我极少用”===“,并且避免在不同类型变量之间进行比较。

二、”==“作用规则

a == b,如果a、b类型相同,那很简单,值相同即为true,不同即为false。所以这里只讨论a、b类型不同的情况——虽然应该避免不同类型变量相比较,但是弄明白”比较中的隐式类型转换“却非常必要。

参照MDN文档梳理了一下不同类型的的值比较的规则:

    • 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
    • 如果操作数之一是Boolean,则将布尔操作数转换为1或0。
    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()toString()方法将对象转换为数字或字符串
    • null == undefined为true,此外通常情况下null和undefined与其它对象都不相等。

可以看到,前三条规则中,都是试图转变为字符串和数字进行比较,在比较中,可以把布尔值当成数字。回到刚才的问题,”![] == []“就比较容易理解了,相当于"false == []",有Boolean操作数,先转为数字,相当于比较”0 == []“,而”[]“转为数字是0,所以返回true。

MDN文档细致给出了不同类型变量的比较,表格容易理解却远不如规则好记:

被比较值 B
Undefined Null Number String Boolean Object
被比较值 A Undefined true true false false false IsFalsy(B)
Null true true false false false IsFalsy(B)
Number false false A === B A === ToNumber(B) A=== ToNumber(B) A== ToPrimitive(B)
String false false ToNumber(A) === B A === B ToNumber(A) === ToNumber(B) ToPrimitive(B) == A
Boolean false false ToNumber(A) === B ToNumber(A) === ToNumber(B) A === B ToNumber(A) == ToPrimitive(B)
Object false false ToPrimitive(A) == B ToPrimitive(A) == B ToPrimitive(A) == ToNumber(B)

A === B

在上面的表格中,ToNumber(A) 尝试在比较前将参数 A 转换为数字,这与 +A(单目运算符+)的效果相同。ToPrimitive(A)通过尝试调用 A 的A.toString() 和 A.valueOf() 方法,将参数 A 转换为原始值(Primitive)。

三、对象的valueOf和toString,转换的时候到底用哪个?

Object对象在隐式转换的时候,会尝试调用valueOf和toString函数,向字符串或者数字转换。那优先会采用哪一个函数的值呢?

测试后发现:如果valueOf或者toString返回原始值(”String“、”Number“、”Boolean“、”null“、”undefined“),按valueOf > toString的优先级取得返回值,若返回值是”null“或者”undefined“,比较返回false,否则根据另一个比较值转为字符串或者数字进行比较;如果valueOf和toString均不返回原始值,则比较操作将会报错!

const a = {}
a.valueOf = () => 1
a.toString = () => 2
console.log(a == 1, a == 2) // true, false

const b = {}
b.valueOf = () => null // 优先级高于toString,比较直接返回false
b.toString = () => '1'
console.log(b == 'null', b == 1, b == '1') // false, false, false

const c = {}
c.valueOf = () => ([]) // 返回非基本值,将尝试取toString比较
c.toString = () => '1'
console.log(c == 'undefined', c == '1') // false, true

const d = {}
d.valueOf = () => ([]) // 返回非基本值
d.toString = () => ([])
console.log(d == 'undefined', d == '1') // 比较报错:不能转为原始值

很明显,根据valueOf > toString的优先级可以看到,objA == 'abc' 的比较并不同于简单地将objA显式转换为字符串进行比较,即:objA == 'abc' 与 String(objeA) == 'abc' 结果并不一定相同(显式转换直接走toString):

const e = {}
e.valueOf = () => 1
e.toString = () => '2'
console.log(e == 1, e == '1', String(e) == '1',  String(e) == '2') // true, true, false, true

四、更高优先级的转换函数:ES6对象的Symbol.toPrimitive属性

除了valueOf、toString函数外,ES6规范提出了Symbol.toPrimitive作为对象属性名,其值是一个函数,函数定义了对象转为字符串、数字等原始值的返回值,其优先级要高于valueOf、toString。

        Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。该函数被调用时,会被传递一个字符串参数 hint ,表示要转换到的原始值的预期类型。 hint 参数的取值是 "number""string" 和 "default" 中的任意一个。对于”==“操作符,hint传递的值是”default“。

const a = {}
a[Symbol.toPrimitive] = (hint) => {
    if (hint == 'number') return 1
    if (hint == 'string') return 2
    return 3
}
a.valueOf = () => 4
a.toString = () => 5
console.log(a == 1, a == 2, a == 3, a == 4, a == 5) // false, false, true, false, false

如果使用Number或者String强制转换a,则传入的”hint“会是”number“或者”string“。

 五、总结

隐式类型转换确实是比较容易忽略的问题,毕竟通常很少用得到。本文介绍的valueOf、toString、[Symbol.toPrimitive]等函数,都可以干涉到类型转换的结果。想起以前网上见到的一道面试题:如何让a == 1 && a == 2 && a == 3同时成立?使用上述几个函数应该很简单就可以解答这个问题。

¥赞赏