JavaScirpt的隐式类型转换

474 阅读4分钟

前言

曾经有人问我,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

这段代码涉及到了两个概念:

  • 隐式类型转换
  • ObjectvalueOf函数

我们知道 == 会存在隐式类型转换的问题,比如常见的我们在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 == 3true呢?

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,两者的类型不同,yBoolean类型。[]是一个对象,所以对应转换成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/…

www.ecma-international.org/ecma-262/5.…

读懂 ECMAScript 规格 JavaScript 中的相等性判断