@gyyin
2019-10-03T14:12:13.000000Z
字数 5593
阅读 300
慕课专栏
JavaScript 中的类型转换一直都是让前端开发者最头疼的问题。前阵子,推特上有个人专门发了一张图说 JavaScript 让人不可思议。
除了这个,还有很多经典的、让 JavaScript 开发者摸不着头脑的类型转换,譬如下面这些,你是否知道结果都是多少?
1 + {} === ?
{} + 1 === ?
1 + [] === ?
1 + '2' === ?
本文将带领你从 ECMA 规范开始,去深入理解 JavaScript 中的类型转换,让类型转换不再成为前端开发中的拦路虎。
JS中有六种简单数据类型:undefined、null、boolean、string、number、symbol,以及一种复杂类型:object。
但是 JavaScript 在声明时只有一种类型,只有到运行期间才会确定当前类型。在运行期间,由于 JavaScript 没有对类型做严格限制,导致不同类型之间可以进行运算,这样就需要允许类型之间互相转换。
显式类型转换就是手动地将一种值转换为另一种值。一般来说,显式类型转换也是严格按照上面的表格来进行类型转换的。
常用的显式类型转换方法有Number、String、Boolean、parseInt、parseFloat、toString等等。
这里需要注意一下parseInt,有一道题偶尔会在面试中遇到。
问:为什么 [1, 2, 3].map(parseInt) 返回 [1,NaN,NaN]?
答:parseInt函数的第二个参数表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
一般来说,类型转换主要是基本类型转基本类型、复杂类型转基本类型两种。
转换的目标类型主要分为以下几种:
我参考了ECMA262的官方文档来总结一下这几种类型转换。ECMA 文档链接:ECMA-262
其他类型转换到number类型的规则见下方表格:
原始值 | 转换结果 |
---|---|
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类型同样可以用构造函数String来实现。
String(undefined) // 'undefined'
String(true) // 'true'
String(false) // 'false'
String(11) // '11'
String(Symbol()) // 'Symbol()'
String({})
隐式类型转换一般是在涉及到运算符的时候才会出现的情况,比如我们将两个变量相加,或者比较两个变量是否相等。
隐式类型转换其实在我们上面的例子中已经有所体现。对于对象转原始类型的转换,也会遵守ToPrimitive的规则,下面会进行细说。
在对象转原始类型的时候,一般会调用内置的ToPrimitive方法,而ToPrimitive方法则会调用OrdinaryToPrimitive方法,我们可以看一下ECMA的官方文档。
我来翻译一下这段话。
ToPrimitive方法接受两个参数,一个是输入的值input,一个是期望转换的类型PreferredType。
而OrdinaryToPrimitive方法也接受两个参数,一个是输入的值O,一个也是期望转换的类型hint。
如果只用文字来描述,你肯定会觉得过于晦涩难懂,所以这里我就自己用代码来实现这两个方法帮助你的理解。
// 获取类型
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的规范去理解这些转换规则的原理,那么就会很容易知道为什么最后会得到那些结果。