JavaScript 中的类型转换一直都是让前端开发者最头疼的问题。前阵子,推特上有我的专门发了一张图说 JavaScript 让人难以想象。前端
除了这个,还有不少经典的、让 JavaScript 开发者摸不着头脑的类型转换,譬以下面这些,你是否知道结果都是多少?面试
1 + {} === ? {} + 1 === ? 1 + [] === ? 1 + '2' === ?
本文将带领你从 ECMA 规范开始,去深刻理解 JavaScript 中的类型转换,让类型转换再也不成为前端开发中的拦路虎。函数
JS 中有六种简单数据类型:undefined
、null
、boolean
、string
、number
、symbol
,以及一种复杂类型:object
。
可是 JavaScript 在声明时只有一种类型,只有到运行期间才会肯定当前类型。在运行期间,因为 JavaScript 没有对类型作严格限制,致使不一样类型之间能够进行运算,这样就须要容许类型之间互相转换。spa
显式类型转换就是手动地将一种值转换为另外一种值。通常来讲,显式类型转换也是严格按照上面的表格来进行类型转换的。prototype
经常使用的显式类型转换方法有 Number
、String
、Boolean
、parseInt
、parseFloat
、toString
等等。
这里须要注意一下 parseInt
,有一道题偶尔会在面试中遇到。翻译
问:为何 [1, 2, 3].map(parseInt) 返回 [1,NaN,NaN]?
答:parseInt函数的第二个参数表示要解析的数字的基数。该值介于 2 ~ 36 之间。若是省略该参数或其值为 0,则数字将以 10 为基础来解析。若是它以 “0x” 或 “0X” 开头,将以 16 为基数。3d
若是该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
通常来讲,类型转换主要是基本类型转基本类型、复杂类型转基本类型两种。
转换的目标类型主要分为如下几种:code
string
number
boolean
我参考了 ECMA-262 的官方文档来总结一下这几种类型转换。ECMA 文档连接:ECMA-262对象
其余类型转换到 number
类型的规则见下方表格:blog
原始值 | 转换结果 |
---|---|
Undefined | NaN |
Null | 0 |
true | 1 |
false | 0 |
String | 根据语法和转换规则来转换 |
Symbol | Throw a TypeError exception |
Object | 先调用toPrimitive,再调用toNumber |
String
转换为 Number
类型的规则:
使用+
能够将其余类型转为 number
类型,咱们用下面的例子来验证一下。
+undefined // NaN +null // 0 +true // 1 +false // 0 +'111' // 111 +'0x100F' // 4111 +'' // 0 'b' + 'a' + + 'a' + 'a' // 'baNaNa' +Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
原始值 | 转换结果 |
---|---|
Undefined | false |
Boolean | true or false |
Number | 0和NaN返回false,其余返回true |
Symbol | true |
Object | true |
咱们也可使用 Boolean
构造函数来手动将其余类型转为 boolean
类型。
Boolean(undefined) // false Boolean(1) // true Boolean(0) // false Boolean(NaN) // false Boolean(Symbol()) // true Boolean({}) // true
原始值 | 转换结果 | |
---|---|---|
Undefined | 'Undefined' | |
Boolean | 'true' or 'false' | |
Number | 对应的字符串类型 | |
String | String | |
Symbol | Throw a TypeError exception | |
Object | 先调用toPrimitive,再调用toNumber |
转换到 string
类型能够用模板字符串来实现。
`${undefined}` // 'undefined' `${true}` // 'true' `${false}` // 'false' `${11}` // '11' `${Symbol()}` // Cannot convert a Symbol value to a string `${{}}`
隐式类型转换通常是在涉及到运算符的时候才会出现的状况,好比咱们将两个变量相加,或者比较两个变量是否相等。
隐式类型转换其实在咱们上面的例子中已经有所体现。对于对象转原始类型的转换,也会遵照 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
方法也接受两个参数,一个是输入的值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
,不然就抛出一个类型错误的报错。若是只用文字来描述,你确定会以为过于晦涩难懂,因此这里我就本身用代码来实现这两个方法帮助你的理解。
// 获取类型 const getType = (obj) => { return Object.prototype.toString.call(obj).slice(8,-1); } // 是否为原始类型 const isPrimitive = (obj) => { const types = ['String','Undefined','Null','Boolean','Number']; return types.indexOf(getType(obj)) !== -1; } const ToPrimitive = (input, preferredType) => { // 若是input是原始类型,那么不须要转换,直接返回 if (isPrimitive(input)) { return input; } let hint = '', exoticToPrim = null, methodNames = []; // 当没有提供可选参数preferredType的时候,hint会默认为"default"; if (!preferredType) { hint = 'default' } else if (preferredType === 'string') { hint = 'string' } else if (preferredType === 'number') { hint = 'number' } exoticToPrim = input.@@toPrimitive; // 若是有toPrimitive方法 if (exoticToPrim) { // 若是exoticToPrim执行后返回的是原始类型 if (typeof (result = exoticToPrim.call(O, hint)) !== 'object') { return result; // 若是exoticToPrim执行后返回的是object类型 } else { throw new TypeError('TypeError exception') } } // 这里给了默认hint值为number,Symbol和Date经过定义@@toPrimitive方法来修改默认值 if (hint === 'default') { hint = 'number' } return OrdinaryToPrimitive(input, hint) } const OrdinaryToPrimitive = (O, hint) => { let methodNames = null, result = null; if (typeof O !== 'object') { return; } // 这里决定了先调用toString仍是valueOf if (hint === 'string') { methodNames = [input.toString, input.valueOf] } else { methodNames = [input.valueOf, input.toString] } for (let name in methodNames) { if (O[name]) { result = O[name]() if (typeof result !== 'object') { return result } } } throw new TypeError('TypeError exception') }
总结一下,在进行类型转换的时候,通常是经过 ToPrimitive
方法将引用类型转为原始类型。若是引用类型上有 @@toPrimitive
方法,就调用 @@toPrimitive
方法,执行后的返回值为原始类型就直接返回,若是依然是对象,那么就抛出报错。
若是对象上没有 toPrimitive
方法,那么就根据转换的目标类型来判断先调用 toString
仍是 valueOf
方法,若是执行这两个方法后获得了原始类型的值,那么就返回。不然,将会抛出错误。
在 ES6 以后提供了 Symbol.toPrimitive
方法,该方法在类型转换的时候优先级最高。
const obj = { toString() { return '1111' }, valueOf() { return 222 }, [Symbol.toPrimitive]() { return 666 } } const num = 1 + obj; // 667 const str = '1' + obj; // '1666'
也许上面关于 ToPrimitive
的代码讲解你仍是会以为晦涩难懂,那我接下来就举几个例子来讲明对象的类型转换。
var a = 1, b = '2'; var c = a + b; // '12'
也许你会好奇,为何不是将后面的 b
转换为 number
类型,最后获得3?
咱们仍是要先看文档对加号的定义。
首先会分别执行两个值的 toPrimitive
方法,由于 a
和 b
都是原始类型,因此仍是获得了1和'2'。
从图上看到若是转换后的两个值的 Type
有一个是 String
类型,那么就将两个值通过 toString
转换后串起来。所以最后获得了'12',而不是3。
咱们还能够再看一个例子。
var a = 'hello ', b = {}; var c = a + b; // "hello [object Object]"
这里还会分别执行两个值的 toPrimitive
方法,a
仍是获得了'hello ',而b因为没有指定preferredType,因此会默认被转为 number
类型,先调用 valueOf
,但 valueOf
仍是返回了一个空对象,不是原始类型,因此再调用 toString
,获得了 '[object Object]'
,最后将二者链接起来就成了 "hello [object Object]"
。
若是咱们想返回 'hello world'
,那该怎么改呢?只须要修改 b
的 valueOf
方法就行了。
b.valueOf = function() { return 'world' } var c = a + b; // 'hello world'
也许你在面试题中看到过这个例子。
var a = [], b = []; var c = a + b; // ''
这里为何 c
最后是''呢?由于 a
和 b
在执行 valueOf
以后,获得的依然是个 []
,这并不是原始类型,所以会继续执行 toString
,最后获得'',两个''相加又获得了''。
咱们再看一个指定了 preferredType
的例子。
var a = [1, 2, 3], b = { [a]: 111 }
因为 a
是做为了 b
的键值,因此 preferredType
为 string
,这时会调用 a.toString
方法,最后获得了'1,2,3'
类型转换一直是学 JS 的时候很难搞明白的一个概念,由于转换规则比较复杂,常常让人以为莫名其妙。可是若是从 ECMA 的规范去理解这些转换规则的原理,那么就会很容易知道为何最后会获得那些结果。