JS中的隐式转换
// 问题 如何让该片段在控制台输出"hello world"
if (a == 1 && a == 2 && a == 3) {
console.log('hello world!');
}
/*
a == 1 ?
a == 2 ?
a == 3 ?
a为什么能等于这么多值???
带着这样的问题,我们来看一下JS中的隐式转换
*/
JS是一门弱类型语言,在声明变量的时候不需要声明变量的类型,并且不同类型的数据可以进行计算,这都得益于JS中在语言设计时的创建的隐式转换。
我们知道JS有7大数据类型
分为基本类型(原始值):String Number Boolean null undefined Symbol
复杂引用类型(引用值):Object
隐式转换作为JS中的一个难点,因为js在一些特定操作符的情况下会调用隐式转换,这也是js灵活的地方,两个操作符"+"和"=="是比较常见的出现隐式转换的操作符
+ == 这两个符号可以是无视类型并且针对多种类型可以转化成多种类型去计算。
- * / 这三个只针对Number类型进行隐式转换,故而转化时只考虑Number类型即可
一、既然是要隐式转换,那麽必然有一套约定俗成的规则,而不是随便转化。
隐式转换主要分为三种方式
- 将值转化为原始值 => toPrimitive
- 将值转化为String => ToString
- 将值转化为Number => ToNumber
2.1 通过toPrimitive将值转化为原始值
js引擎内部有一个抽象操作ToPrimitive,他有着这样的签名, ToPrimitive(input, PreferredType?) input是传入的值,PreferredType是可选参数,表示转化为什么类型,转化后的结果并不一定是这个参数所指定的结果,但是一定是一个原始类型的值
-
如果PreferredType被标记为Number,则会进行如下操作进行转化
- 如果输入的值已经是一个原始值,则直接返回该值
- 否则如果是一个对象,则调用对象的valueOf()方法,如果valueOf()返回的是一个原始值,则直接返回得到的值
- 否则调用这个对象的toString()方法,如果toString()返回的是一个原始值,则直接返回该值
- 否则抛出TypeError异常
-
如果PreferredType被标记为String,则会进行如下操作转化
- 如果输入的值是一个原始值,则直接返回该值
- 否则如果是一个对象,则调用对象的toString()方法,如果toString()方法返回的是一个原始值,则直接返回该值
- 否则调用这个对象的valueOf()方法,如果valueOf()返回的是一个原始值,则直接返回该值
- 否则抛出TypeError异常
-
既然PreferredType是一个可选值,那么不传默认会被标记为什么呢
- 如果传入值为Date类型,则标记为String
- 如果传入的值为其他引用类型,则标记为Number
-
上面主要提到了valueOf和toString方法,那么我们能否保证任何情况下都有这两个方法呢,答案是肯定的
只要是引用类型的我们可以直接顺着原型链找到Object.prototype,我们会发现在原型上就有toString和valueOf方法,故而是一定可以找到的
-
先看看对象的valueOf函数,其转化的结果是什么
对于js的常见内置对象:
Date, Array, Math, Number, Boolean, String, Array, RegExp, Function
。-
Number、Boolean、String这三种构造函数生成的基础值的对象形式,通过valueOf转换后会变成相应的原始值。如:
var num = new Number('12'); num.valueOf(); // 12 var str = new String('44rr'); str.valueOf(); // '44rr' var bool = new Boolean('xxx'); bool.valueOf(); // true
-
Date是一个特殊的对象,通过valueOf会转化为毫秒值
var a = new Date(); a.valueOf(); // 1614700725143
-
其他的都返回this,也是引用本身
var a = new Array(); a.valueOf() === a; // true var b = new Object({}); b.valueOf() === b; // true
-
-
再看看toString函数,其转换的结果是什么?对于js的常见内置对象:
Date, Array, Math, Number, Boolean, String, Array, RegExp, Function
。-
Number、Boolean、String、Array、Date、RegExp、Function这几种构造函数生成的对象,通过toString转换后会变成相应的字符串的形式,因为这些构造函数上封装了自己的toString方法。如:
Number.prototype.hasOwnProperty('toString'); // true Boolean.prototype.hasOwnProperty('toString'); // true String.prototype.hasOwnProperty('toString'); // true Array.prototype.hasOwnProperty('toString'); // true Date.prototype.hasOwnProperty('toString'); // true RegExp.prototype.hasOwnProperty('toString'); // true Function.prototype.hasOwnProperty('toString'); // true var num = new Number(123) num.toString() // '123' var boolean = new Boolean('xxx') boolean.toString() // 'true' var string = new String('abc123') string.toString() // 'abc123' var array = new Array(1,2,3,4) array.toString() // '1,2,3,4' var date = new Date() date.toString() // "Wed Mar 03 2021 00:15:57 GMT+0800 (中国标准时间)" var RegExp = new RegExp(/\w/g) RegExp.toString() // '/\w/g' var fun = new Function('console.log(1)') fun.toString() // "function anonymous() {console.log(1)}" var fun1 = function fun1() {} fun1.toString() // "function fun1() {}"
2. 除了上述对象以外,其他的对象都是返回的该对象的类型,调用了原型链上的Object.prototype.toString()方法 ```js var obj = {} obj.toString() // '[Object object]' Math.toString() // '[Object Math]'
-
-
一些思考
从上面valueOf和toString两个函数对对象的转换可以看出为什么对于ToPrimitive(input, PreferredType?),PreferredType没有设定的时候,除了Date类型,PreferredType被设置为String,其它的会设置成Number。
- 因为对于我们不知道要转换什么类型的时候,最好是返回其本身,标记为Number先调用valueOf的时候除了在String、Number、Boolean生成的对象字面量转化为对应的原始类型值,还有Date返回了对应的毫秒值以外,其他都返回了本身this
- 为什么默认使用标记为Number呢,因为Number会先调用valueOf(),而不是像标记为String无脑调用toString方法转化为字符串
- 为什么Date在没有标记的时候默认标记为String先调用toString()方法,因为我们不知道要转化为什么,所以没必要转化为一个特别大的数字,所有调用toString转化为String
2.2 通过ToNumber将值转换为数字
undefined => NaN
null => +0
boolean => false: 0, true: 1
string => '' : 0, 有字符串数字解析为数字,e.g. '12.3': 12.3,'xxx': NaN
number => number 无需转换
object => 先ToPrimivetive(obj, preferredType=Number),然后再进行 ToNumber
2.3 通过ToString将值转化为字符串
undefined => 'undefined'
null => 'null'
boolean => false: 'false', true: 'true'
string => string 无需转化
number => 123: '123'
object => 先ToPrimitive(obj, preferredType=String), 然后进行Tostring
二、从理论到实践去看JS的隐式转换
-
从第一章理论知识和规则我们已经了解了js是如何对不同数据类型进行隐式转换的,下面我们来几道题目实战一下
({}+{}) /* 1. 分析两个数据类型为object,再看操作符为“+”,则需要转化为原始类型才可以进行加和 2. 采用抽象函数ToPrimitive(input), 由于没有明确指定需要转化的值,故而默认为Number,先调用内部的valueOf()进行转化,({}).valueOf(),由于是object对象则返回this,没有转化为原始值继续调用({}).toString,转化为'[Object object]',转化为了原始值结束返回 3. 则式子变成'[Object object]'+'[Object object]' = '[Object object][Object object]' */ ({}+[]).length /* 1. 操作符优先级()优先级大于点,先执行括号内的 2. 发现内部为两个引用类型的数据,操作符为"+",则需要调用抽象函数ToPrimitive(input),进行转化 3. 没有明确指定转化的目标类型,默认为Number,则先调用valueOf() 4. ({}).valueOf() => 返回this {}, ([]).valueOf() => 返回this 5. 没有转化为原始值类型,对valueOf()的返回结果继续调用toString() 6. ({}).toString() => '[Object object]', ([]).toString() => 数组的原型实现了自己的toString()方法,会将数据通过类似join()的方式转化为字符串,[] 其实和new Array() 一样,内部没有传值为空,转化为'' 7. 都转化为了原始值,则式子变为('[Object object]' + "").length 8. '[Object object]'.length => 15 */ {}+[].length /* 1. {}在第一位js会编译为作用域,则变为+[].length 2. .操作符优先级大于+,[].length === 0 , +0 = 0 */ {}+{}.length /* 1. .操作符优先级大于+, 则{} + undefied 2. {}在第一位会编译为作用域,则+undefined 3. undefined 转化为number NaN */ ({}+{}).length /* 1. 上面问题1得到了一个结果15,两个15*2=30 */ {}+[] /* 1. 同样是作用域,+[] 2. 转化为Number ([]).valueOf() => [] 3. 继续([]).toString() => '' 4. +"", ""要转化为Number转化为了0 5. +0 = 0 */ ({}+[]) /* 1. ()操作符优先级21最高,内部{}+[]不会编译为作用域 2. ({}).valueOf().toString() === '[Object object]' 3. ([]).valueOf().toString() === '' 4. '[Object object]' */ []+{} /* 1. 同上 ""+"[Object object]" */
-
== 运算符的隐式转换
// 比较运算x==y,其中x和y是值,返回true或者false。例如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。
总结一下:
上面主要分为两类
-
类型相同
类型相同时,没有类型转换,注意NaN不与任何值相等即可
-
类型不同
- x,y 为null、undefined两者中的一个 返回true
- x,y为Number和String类型时,转换为Number进行比较
- 有Boolean时,转化为Number比较
- 一个Object类型,一个String或者Number类型,将Object按类型转换为原始值后,再按上面三步比较
看一些例子:
var a = { valueOf: function () { return 1; }, toString: function () { return '123' } } true == a /* 1. 首先得知true和a不是一个类型,则需要类型转换 2. 有Boolean则转化为Number进行比较 3. true => 1, a => Object没有指定先调用valueOf(), 返回了1,得到原始值,则返回 3. 1 == 1 // true */ // 再看一段复杂一点的比较 [] == !{} /* 1. 操作符优先级!优先级17 , == 优先级11,则先运算!{} 2. !{} => false 3. 式子变为 [] == false , 有false则转化为Number 4. false => 0, []转化为原始值再转Number,([]).valueOf().toString() => '' => 0 5. 0 == 0 // true */
现在我们来看看最开始的那道题目,我们是不是有些思路了
if (a == 1 && a == 2 && a == 3) { console.log('hello world!'); } /* 1. 分析问题,a == 1 , a == 2, a == 3,可以得知,a是一个动态更新自己的变量,js中没有这种变量,肯定是一种比较trick的写法,又看到 == 操作符,则想到了隐式转换中会依次隐式调用两个函数,valueOf和toString 2. 方法在原型上,我们可以写在自身上进行拦截从而达到目的。 3. 可推出a是一个拥有valueOf或者toString的方法,并且有一个值可以取出来并且随着从左到右依次执行的 == 运算符依次执行+1 4. 我们可推算出a如下,每次调用隐式转换都将++, 即 1,2,3 依次相等,最终输出hello world */ let a = { value: 0, valueOf() { this.value++ return this.value } }
-
总结
其实当我们了解了隐式转换发生的规则以后,再看这些题目就会觉得非常简单,js作为一门弱类型的语言,存在着这样的隐式转换,这些可以给我们提供一些便利性,但是在日常的开发中,并不推荐使用这种隐式转换,因为它令人感到晦涩难懂不够直接,日常工作中还是要避免使用==,采用===进行判断值的相等于否。