`${undefined}` // 'undefined'
`${true}` // 'true'
`${false}` // 'false'
`${11}` // '11'
`${Symbol()}` // Cannot convert a Symbol value to a string
`${{}}`
如果 exoticToPrim 不是 Undefined,那么就让 result 等于 Call(exoticToPrim, input, « hint »),意思就是执行 exoticToPrim(hint),如果执行后的结果 result 是原始数据类型,返回 result,否则就抛出类型错误的异常
前言
JavaScript 中的类型转换一直都是让前端开发者最头疼的问题。前阵子,推特上有个人专门发了一张图说 JavaScript 让人不可思议。
除了这个,还有很多经典的、让 JavaScript 开发者摸不着头脑的类型转换,譬如下面这些,你是否知道结果都是多少?
本文将带领你从 ECMA 规范开始,去深入理解 JavaScript 中的类型转换,让类型转换不再成为前端开发中的拦路虎。
数据类型
JS 中有六种简单数据类型:
undefined
、null
、boolean
、string
、number
、symbol
,以及一种复杂类型:object
。 但是 JavaScript 在声明时只有一种类型,只有到运行期间才会确定当前类型。在运行期间,由于 JavaScript 没有对类型做严格限制,导致不同类型之间可以进行运算,这样就需要允许类型之间互相转换。类型转换
显式类型转换
显式类型转换就是手动地将一种值转换为另一种值。一般来说,显式类型转换也是严格按照上面的表格来进行类型转换的。
常用的显式类型转换方法有
Number
、String
、Boolean
、parseInt
、parseFloat
、toString
等等。 这里需要注意一下parseInt
,有一道题偶尔会在面试中遇到。string
number
boolean
我参考了 ECMA-262 的官方文档来总结一下这几种类型转换。ECMA 文档链接:ECMA-262
ToNumber
其他类型转换到
number
类型的规则见下方表格:String
转换为Number
类型的规则:使用
+
可以将其他类型转为number
类型,我们用下面的例子来验证一下。ToBoolean
我们也可以使用
Boolean
构造函数来手动将其他类型转为boolean
类型。ToString
转换到
string
类型可以用模板字符串来实现。隐式类型转换
隐式类型转换一般是在涉及到运算符的时候才会出现的情况,比如我们将两个变量相加,或者比较两个变量是否相等。 隐式类型转换其实在我们上面的例子中已经有所体现。对于对象转原始类型的转换,也会遵守
ToPrimitive
的规则,下面会进行细说。从ES规范来看类型转换
ToPrimitive
在对象转原始类型的时候,一般会调用内置的
ToPrimitive
方法,而ToPrimitive
方法则会调用OrdinaryToPrimitive
方法,我们可以看一下 ECMA 的官方文档。我来翻译一下这段话。
ToPrimitive
方法接受两个参数,一个是输入的值input
,一个是期望转换的类型PreferredType
。如果没有传入
PreferredType
参数,让hint
等于"default"如果
PreferredType
是hint String
,让hint
等于"string"如果
PreferredType
是hint Number
,让hint
等于"number"让
exoticToPrim
等于GetMethod(input, @@toPrimitive)
,意思就是获取参数input
的@@toPrimitive
方法如果
exoticToPrim
不是Undefined
,那么就让result
等于Call(exoticToPrim, input, « hint »)
,意思就是执行exoticToPrim(hint)
,如果执行后的结果result
是原始数据类型,返回result
,否则就抛出类型错误的异常如果
hint
是"default",让hint
等于"number"返回
OrdinaryToPrimitive(input, hint)
抽象操作的结果OrdinaryToPrimitive
而
OrdinaryToPrimitive
方法也接受两个参数,一个是输入的值O,一个也是期望转换的类型hint
。如果输入的值是个对象
如果
hint
是个字符串并且值为'string'或者'number'如果
hint
是'string',那么就将methodNames
设置为toString
、valueOf
如果
hint
是'number',那么就将methodNames
设置为valueOf
、toString
遍历
methodNames
拿到当前循环中的值name
,将method
设置为O[name]
(即拿到valueOf
和toString
两个方法)如果
method
可以被调用,那么就让result
等于method
执行后的结果,如果result
不是对象就返回result
,否则就抛出一个类型错误的报错。ToPrimitive 的代码实现
如果只用文字来描述,你肯定会觉得过于晦涩难懂,所以这里我就自己用代码来实现这两个方法帮助你的理解。
总结一下,在进行类型转换的时候,一般是通过
ToPrimitive
方法将引用类型转为原始类型。如果引用类型上有@@toPrimitive
方法,就调用@@toPrimitive
方法,执行后的返回值为原始类型就直接返回,如果依然是对象,那么就抛出报错。如果对象上没有
toPrimitive
方法,那么就根据转换的目标类型来判断先调用toString
还是valueOf
方法,如果执行这两个方法后得到了原始类型的值,那么就返回。否则,将会抛出错误。Symbol.toPrimitive
在 ES6 之后提供了
Symbol.toPrimitive
方法,该方法在类型转换的时候优先级最高。例子
也许上面关于
ToPrimitive
的代码讲解你还是会觉得晦涩难懂,那我接下来就举几个例子来说明对象的类型转换。也许你会好奇,为什么不是将后面的
b
转换为number
类型,最后得到3? 我们还是要先看文档对加号的定义。首先会分别执行两个值的
toPrimitive
方法,因为a
和b
都是原始类型,所以还是得到了1和'2'。 从图上看到如果转换后的两个值的Type
有一个是String
类型,那么就将两个值经过toString
转换后串起来。因此最后得到了'12',而不是3。我们还可以再看一个例子。
这里还会分别执行两个值的
toPrimitive
方法,a
还是得到了'hello ',而b由于没有指定preferredType,所以会默认被转为number
类型,先调用valueOf
,但valueOf
还是返回了一个空对象,不是原始类型,所以再调用toString
,得到了'[object Object]'
,最后将两者连接起来就成了"hello [object Object]"
。 如果我们想返回'hello world'
,那该怎么改呢?只需要修改b
的valueOf
方法就好了。也许你在面试题中看到过这个例子。
这里为什么
c
最后是''呢?因为a
和b
在执行valueOf
之后,得到的依然是个[]
,这并非原始类型,因此会继续执行toString
,最后得到'',两个''相加又得到了''。 我们再看一个指定了preferredType
的例子。由于
a
是作为了b
的键值,所以preferredType
为string
,这时会调用a.toString
方法,最后得到了'1,2,3'总结
类型转换一直是学 JS 的时候很难搞明白的一个概念,因为转换规则比较复杂,经常让人觉得莫名其妙。 但是如果从 ECMA 的规范去理解这些转换规则的原理,那么就会很容易知道为什么最后会得到那些结果。