在面试中经常遇到特别奇葩的js隐式转换的面试题,可能在实际业务中遇到这种情况较少,然而深入理解其转换原理,能帮助我们更好的了解js。
js中有8种数据类型,分为原始类型和对象类型
//基础类型(原始值)
//undefined、 Null、
//String、 Number、 Boolean、 Symbol 、 BigInt(Symbol,BigInt为新出的,本文不讨论这种类型)
//复杂类型(对象值)
//object
js在一些操作符下会做一些类型转换,比如:+,==等
隐式转换中主要涉及到三种转换:
1、将值转为原始值,ToPrimitive()。
2、将值转为数字,ToNumber()。
3、将值转为字符串,ToString()。
1.通过ToPrimitive将值转化为原始值:
ToPrimitive(input,preferredType)接收两个参数,第一个input是要转换的值,第二个preferredType为可选参数,可以是Number或String类型。他只是一个转换标志,转化后的结果并不一定是这个参数所值的类型,但是转换结果一定是一个原始值(或者报错)。那么ToPrimitive是如何对输入进行类型转化的呢?首先如果PreferredType传为Number:
1、如果输入的值已经是一个原始值,则直接返回它
2、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
3、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。
如果PreferredType传为String:
1、如果输入的值已经是一个原始值,则直接返回它
2、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
3、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。
如果不传:
PreferredType会被默认设置为Number,但是当输入input为Date类型,则PreferredType被设置为String
为什么ToPrimitive在除了date时设置PreferredType为string,而其他时候设置为number呢,按照上述规则,当PreferredType为number时,会先调用valueof方法,若不是原始值则调用tostring方法,这样做的好处就是最大可能的保证将传入的值保持为原有值,而PreferredType设置为string则先调用tostring一股脑的将传入转为string。但是当日期为date格式时,valueof方法会取到日期的时间戳,我们获取时间戳那么大一个数字显然没太大意义,而tostring获取的是日期格式,所以input为date时,设置PreferredType为string更好,看下面代码就能明白。
var d = new Date();
d.toString(); // "Wed Oct 11 2017 08:00:00 GMT+0800 (中国标准时间)"
d.valueOf();//1598168742145
例子:
上面的文字看起来不是很清晰,来看一些例子:
{} + {} //???
两个空对象进行+运算符,肯定要先进行隐式类型转换才能进行计算。
按照上述规则:
* 首先是js内部使用ToPrimitive,由于我们没有设置PreferredType,于是PreferredType就是默认值number。
* 所以会执行valueOf方法,({}).valueOf返回的还是{},不是原始值。
* 执行toString()方法,({}).toString返回的是"[object Object]"字符串,是原始值
* 所以结果为"[object Object]" + "[object Object]" = "[object Object][object Object]"
这种数学上的 + - * /等运算的类型转换规律性较强,只需按照ToPrimitive方法执行转为对应的原始值即可。
2. ==运算符
== 运算符的规则较为复杂,按照下面流程来执行,es5文档
比较运算 x==y, 其中 x 和 y 是值,返回 true 或者 false。这样的比较按如下方式进行:
1、若 Type(x) 与 Type(y) 相同, 则
1* 若 Type(x) 为 Undefined, 返回 true。
2* 若 Type(x) 为 Null, 返回 true。
3* 若 Type(x) 为 Number, 则
(1)、若 x 为 NaN, 返回 false。
(2)、若 y 为 NaN, 返回 false。
(3)、若 x 与 y 为相等数值, 返回 true。
(4)、若 x 为 +0 且 y 为 −0, 返回 true。
(5)、若 x 为 −0 且 y 为 +0, 返回 true。
(6)、返回 false。
4* 若 Type(x) 为 String, 则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。 否则, 返回 false。
5* 若 Type(x) 为 Boolean, 当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。
6* 当 x 和 y 为引用同一对象时返回 true。否则,返回 false。
2、若 x 为 null 且 y 为 undefined, 返回 true。
3、若 x 为 undefined 且 y 为 null, 返回 true。
4、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。
5、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。
6、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。
7、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。
8、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。
9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。
10、返回 false。
上面主要分为两类,x、y类型相同时,和类型不相同时。
类型相同时,没有类型转换,主要注意NaN不与任何值相等,包括它自己,即NaN !== NaN。
类型不相同时:
- x,y 为null、undefined两者中一个 // 返回true
- x、y为Number和String类型时,则转换为Number类型比较。
- 有Boolean类型时,Boolean转化为Number类型比较。
- 一个Object类型,一个String或Number类型,将Object类型进行原始转换后,按上面流程进行原始值比较。
例子:
[] == !{}
//
1、! 运算符优先级高于==,故先进行!运算。
2、!{}运算结果为false,结果变成 [] == false比较。
3、根据上面第7条,等式右边y = ToNumber(false) = 0。结果变成 [] == 0。
4、按照上面第9条,比较变成ToPrimitive([]) == 0。
按照上面规则进行原始值转换,[]会先调用valueOf函数,返回this。
不是原始值,继续调用toString方法,x = [].toString() = ''。
故结果为 '' == 0比较。
5、根据上面第5条,等式左边x = ToNumber('') = 0。
所以结果变为: 0 == 0,返回true,比较结束。
最后看一道经典的面试题:
const a = {
i: 1,
toString: function () {
return a.i++;
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('hello world!');
}
1、当执行a == 1 && a == 2 && a == 3 时,会从左到右一步一步解析,首先 a == 1,会进行上面第9步转换。ToPrimitive(a, Number) == 1。
2、ToPrimitive(a, Number),按照上面原始类型转换规则,会先调用valueOf方法,a的valueOf方法继承自Object.prototype。返回a本身,而非原始类型,故会调用toString方法。
3、因为toString被重写,所以会调用重写的toString方法,故返回1,注意这里是i++,而不是++i,它会先返回i,在将i+1。故ToPrimitive(a, Number) = 1。也就是1 == 1,此时i = 1 + 1 = 2。
4、执行完a == 1返回true,会执行a == 2,同理,会调用ToPrimitive(a, Number),同上先调用valueOf方法,在调用toString方法,由于第一步,i = 2此时,ToPrimitive(a, Number) = 2, 也就是2 == 2, 此时i = 2 + 1。
5、同上可以推导 a == 3也返回true。故最终结果 a == 1 && a == 2 && a == 3返回true。
参考:你所忽略的js隐式转换