深入浅出JS的隐式类型转换
深入浅出JS的隐式类型转换
最近在重读《JavaScript高级程序设计》一书,读到基本概念一部分,其中罗列了大量的隐式转换规则,如果没有技巧的话,只能死记硬背,效率低下。本人通过自己的总结,得出了一些原理层面上的规律,可以帮助大家快速理解和记忆这些转换规则。
本文主要分为两部分,第一部分是基础知识——类型转换的规则,这部分比较了解的可以直接去看第二部分——理解各种隐式转换场景,第二部分也是本文所要着重表达的核心思想。
类型转换规则
任意类型转为字符串
转换为字符串,我们分两种情况。分别是对象的toString转换法和String构造函数转换法。他们区别和联系如下:
- 6大数据类型,除了null和undefined,都可以直接调用其toString方法来转为字符串。
- String构造方法,传入undefined和null时分别返回"undefined"和"null",其他传入值相当于调用其toString()方法。
一句话总结:String方法是toString在功能上的超集,String相比toString增加了对null和undefined的支持。
下面是一些例子。注意,如果你不理解为什么数值、布尔值这些不是对象的值也可以调用toString,请搜索“js包装对象”学习有关知识。
//调用String构造方法实际上就是调用传入参数的toString
let obj = {}
obj.toString = () => 'Modified toString'
obj.toString() //等价于String(obj),返回"Modified toString"
//基本数据类型的转换(包装对象)
let num = 1
num.toString() //等价于String(num),返回"1"
true.toString() //等价于String(true),返回"true"
//null和undefied只能用String构造方法转为字符串
null.toString() //报错
String(null) // "null"
undefined.toString() //报错
String(undefined) //undefined
任意类型转为数值
转为数值的情况与转为字符串类似,不过对应的两个方法是对象的valueOf和Number构造函数。请注意,NaN也是一个数值
- 6大数据类型,除了null和undefined,都可以直接调用其valueOf方法来尝试转为其本身所代表的原始数据类型,但是相当一部分时候是不会转为数值的,尤其是对象。
- Number构造方法,一定会把传入的值转化为数值。
Number构造函数对于传入参数的具体操作是:
- 字符串,忽略前导0,转为数值。转换不成功(出现任何不符合数字形式的字符)则为NaN
- 若参数是对象,先调用其valueOf方法,如果返回的不是一个数值,那么调用其toString方法,再将这个字符串作为Number的参数传入,得出最终结果。
- undefined转为NaN,null转为0
- true转为1,false转为0
PS:ES6的Symbol只能转布尔值和字符串,转其他类型全报错。
let a = {}
//未改写前,valueOf返回对象本身
console.log(a.valueOf()) //Object { }
console.log(a.toString()) //"[object Object]"
console.log(Number(a)) //NaN,因为toString的结果"[object Object]"无法转为数值
a.toString = () => '2.22'
console.log(a.valueOf()) //Object { toString: toString() }
console.log(a.toString()) //"2.22"
console.log(Number(a)) //2.22,toString的结果"2.22"转为数值是2.22
a.valueOf = () => 3.33
console.log(a.valueOf()) //3.33
console.log(a.toString()) //"2.22"
console.log(Number(a)) //3.33,valueOf返回了数值,所以直接取valueOf的值作为Number的结果
注意,在隐形类型转换为数值时,一般有两种转换模式(本人自己总结的,权威材料无这两个术语)
- 强转换,用于只能是数值的场景:必须转为数值,即直接传入Number构造函数,无法转换的会返回NaN
- 弱转换,用于可以是数值的场景:对于基本数据类型,传入Number,对于对象,调用其valueOf,如果是数值则返回该数值,如果是其他基本数据类型则传入Number,如果是对象则判定为不能转为数值。
任意类型转为布尔
转为布尔值,我们只需要记忆转为false的那部分。每种数据类型顶多也就一两个,而且都是比较特殊的值,从感性上看这些转为false的值的特点是“空的、不存在的”云云。转为布尔值,要么调用Boolean构造函数,要么使用两个逻辑非运算符,如Boolean(something)
、!!something
- 对于数值,NaN和0转为false
- 对于字符串,空字符串转为false
- null和undefined都转为false
- Symbol(ES6)和对象(不算null)全转为true
理解各种隐式转换场景
理解各种场景的基本思路:理解这个场景到底对什么类型有意义,然后其他值都会被隐性的转为这些类型。
加 +
首先,我们要明确,加法只对数值和字符串有意义,分别对应数值计算和字符串拼接的操作,而且数值运算应是加号的首要任务。
所以,js在看到加运算时,需要判定这个操作是数值计算还是字符串拼接,而且优先尝试数值计算,在无法判定为数值计算时判定为字符串拼接。具体流程如下:
- 首先看是否有字符串,如有任何一方有字符串,那么会判为字符串拼接,会将两方都直接转为字符串。
- 如果两方都不是字符串,那么首先会对双方都进行弱转换*为数值。如果都成功转为数值,则判定为数值计算,按转换后的结果进行加运算;如果任意一方转换结果不是数值,那么本次运算判定为字符串拼接,双方都转为字符串进行拼接
Note:弱转换的定义请参照第一部分的“任意类型转为数值”小节结尾处。
例子:
//只要有一方为字符串,就判定为字符串拼接
'abc' + {} //"abc[object Object]"
'abc' + true //"abctrue"
'abc' + undefined //"abcundefined"
'abc' + null //"abcnull"
'abc' + 123 //"abc123"
//都不为字符串时,先尝试转为数值,转换成功则用这两个数值做数值计算
let a = Object.assign({}, {
valueOf(){
return 1;
}
})
123 + a //124,因为a的valueOf方法返回1
a + a //2
true + a //2,因为true转为数值是1
//当有一方valueOf无法成功转为数值时,判定为字符串拼接,双方都转为字符串
123 + {} //"123[object Object]"
123 + [1, 2, 3, 4] //"1231,2,3,4"
[] + [] //""
⚠️注意:一个加号和一个减号都可以当作单操作数运算符,对于数值是改变其正负性,对于其他数据类型则强转换为数值,再当作数值来对待。所以说,把一个值强制转为数值其实有三种方法,除了上面提到的两种valueOf和Number,还可以+something
减 -
减法只对数值有意义,所以双方都必须转为Number,直接强转换为数值,出现NaN时计算结果也是NaN
补充一点小知识:NaN任何数值计算都返回NaN,任何比较、相等运算都是false,甚至NaN === NaN
也是false
乘*、除/、取模%
这三个运算符也只对数值有意义,所以都必须转为数值。同上。
比较 <、>
比较运算符,对数值和字符串都有意义。对数值来说,就是直接比较数值大小;对字符串来说,是比较其字符的数字编码大小。
实际上,js标准制定者考虑到,就算开发者写出了1 < "6.4"
这样的表达式,开发者的意思多半也是判断数值1和数值6.4的大小。所以,js标准给了字符串比较其相当低的优先级:只有两个操作数都为字符串时,才会判定为字符串的比较。
- 当两个操作数都为字符串,判定为字符串比较,按照从开头开始的每位字符的数字编码逐个比较,比出大小则返回结果,这一位字符相等则比下一位。
- 当有任意一个操作数不是字符串,则两个操作数都强转换为数值,再进行比较。
例子:
'a' < 'c' //字符a的编码于字符c的,返回true
'D' < 'a' //大写字母编码小于小写字母的,返回true
5.55 < '6.66' //true,右边会被转为数值6.66
//对象a既没重写valueOf也没重写toString,转为数值是NaN,所以不管怎么比都是false
let a = {}
5 > a //false
5 < a //false
//重写toString后,valueOf无法转为数值,会将toString的结果传入Number,所以a在比较中相当于4
a.toString = () => '4'
a < 5 //true
a > 5 //false
//重写valueOf后,valueOf返回了数值,不会再去调toString,所以a在比较中相当于7
a.valueOf = () => 7
a < 5 //false
a > 5 //true
相等 ==
要注意相等(==)和全等(===)的区别!全等不会出现类型转换!
相等运算符与比较运算符类似,也主要用于数值。它有两个特殊情况,一个是两个操作数都是对象时,另一个是null和undefined。其他情况还是转为数值进行比较。
- null == undefined,而且这两个值在相等判断中绝对不会出现类型转换
- 对象之间比较引用
- 其他情况优先转为数值(强转换)
null == undefined //true
let a = {}
let b = {}
let c = a
a == b //false,因为引用的不是同一个对象
a == c //true,引用相同
false == 0
true == 1
let obj = {}
obj == 0 //false
obj.toString = () => '2'
obj == 2 //true
if、while等需要布尔值的场景
如果传入的是一个表达式,如something < anotherthing
、thisone == thatone
等,按之前提到的进行判断。
如果是传入的一个值,则传入Boolean强制转为布尔值。比较简单就不举例子了
结语
本博客内容均为本人自己总结的经验,如果有什么不对的地方还请批评指正!