前言
曾经有人问我,JavaScript:(a==1 && a==2 && a==3)
能输出ture
么?年轻的我一脸茫然,感觉既然问我,这个问题就一定有坑,肯定可以输出true
。当时只知道if
判断中 undefined、0、 null
都会被转化为false
,但是,为什么呢?
隐式类型转换
const a = {
num: 0,
valueOf: function() {
return this.num += 1
}
}
const equality = (a == 1 && a == 2 && a == 3)
console.log(equality) // true
这段代码涉及到了两个概念:
- 隐式类型转换
Object
的valueOf
函数
我们知道 == 会存在隐式类型转换的问题,比如常见的我们在if语句中,如果变量值为undefined、0、null等都会被转化为false。而 === 则不存在隐式类型转换的问题。
valueOf
JavaScript
提供了一种将对象转化我原始值的方法:Object.prototype.valueOf()
, 默认情况下,返回正在被调用的对象。
const a = {
num: 0
}
使用valueOf
方法,返回结果如下图:
用typeOf来检测输出结果的类型:
typeof a.valueOf() // 'object'
在控制台打印a
输出结果如下图:
我们发现
valueOf
挂载在__proto__
原型链上。我们将其进行重写:
a.valueOf = function() {
return this.num
}
我们重写了原生的valueOf()方法,当我们调用valueOf的时候,返回a.num,如下:
a.valueOf() // 0
再对其进行验证
typeof a.valueOf() // 'number'
a.num = a.valueOf() // true
这个隐式类型转换为什么很重要呢?因为当两种不同类型值遇到相等操作符的时候,js会对其进行类型转化。
在(a == 1 && a == 2 && a == 3)
中,js
会尝试将对象转化成数字的类型,进行比较。当要转化的是一个Object
的时候,JavaScript
会调用valueOf()
方法。
因为我们重写了valueOf()
方法,那么:
a == 0 // true
我们如何做到让 a == 1 && a == 2 && a == 3
为 true
呢?
a.valueOf = function () {
return this.num += 1 // 每次调用都会将之前的 num进行 +1 操作
}
这样,当我们每次调用valueOf
的时候,它就会将变量增加1
返回给我们。当我们再次运行如下代码,神奇的事情发生了,而这,就是它的运行原理。
const equality = (a == 1 && a == 2 && a == 3)
console.log(equality) // true
- 使用相等操作符,js会做强制类型转化
- 对象每次调用valueOf()的值会增加1
a == 1
a.valueOf() == 1
a.num += 1
0 += 1
a == 1
a.valueOf() == 2
a.num += 1 // 2
1 += 1 // 2
2 == 2
a == 3
a.valueOf() == 3
a.num += 1 // 3
2 += 1 // 3
3 == 3 // true
其它问题
[] == [] // false
[] == ![] // true
{} == !{} // false
{} == ![] // Uncaught SyntaxError: Unexpected token '=='
![] == {} // false
[] == !{} // true
undedined == null // true
我们从 [] == []
和[] == ![]
例子切入分析。
为什么[] == []
是 false
?
我们知道,js对象是引用类型,左边的[]
和右边的[]
虽然看起来长的一样,但是他们引用的地址并不相同,这个是同一种类型的比较。
变量对象与堆内存
基础类型存放在栈(stack)里。
对象类型都放在堆(heap)里。
var a = 20
var b = 'abc'
var c = true
var d = { m: 20 }
var e = { m: 20 }
console.log(d == e) // false
为什么引用值要放在堆中,而原始值要放在栈中:无非是时间换空间,空间换时间的问题,堆比栈大,栈比堆的运算速度快,对象是一个复杂的结构,并且可以自由扩展,如:数组可以无限扩充,对象可以自由添加属性。将他们放在堆中是为了不影响栈的效率。而是通过引用的方式查找到堆中的实际对象再进行操作。相对于简单数据类型而言,简单数据类型就比较稳定,并且它只占据很小的内存。不将简单数据类型放在堆是因为通过引用到堆中查找实际对象是要花费时间的,而这个综合成本远大于直接从栈中取得实际值的成本。所以简单数据类型的值直接存放在栈中。
为什么[] == ![]
是 true
?
我们首先要明白ECMAScript
规范里==
的真正含义: 执行相等比较运算符的结果总是Boolean
类型。表示是否由运算符指定的关系对两操作数成立。
从运算符的优先级我们可以知道 !
取反运算符的优先级会高于==
,所以 ![]
最后会是一个Boolean
类型的值,这点很关键。因此,[]
是 Object
,![]
是 Boolean
,两者的类型不同,y
是Boolean
类型。[]
是一个对象,所以对应转换成Boolean
对象的值为true
,![]
对应的是Boolean
值,就是false
,进而就成了比较 [] === ToNumber(false)
了,也就是 [] == 0
, 也就变为了比较ToPrimitive([]) == 0
,而ToPrimitive([])=''
,最后就变成了 ""==0
,而最后就变成了 toNumber("")==0
的比较了。然后toNumber("")=0
,最终也就变成了 0 == 0
的问题,也就是 []==![]
最后成了0 == 0
的问题,答案显而易见为true
!!!
总结一下==
运算的规则
1. undefined == null // true,且他俩与所有其它值比较的结果都是false
2. String == Boolean // 需要两个操作数同时转为Number
3. String/Boolean == Number // String/Boolean转为Number
4. Object == Primitive // 需要Object转为Primitive(具体通过valueOf和toString方法)。
相关推荐: github.com/jawil/blog/…