一般来说,类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时,然而在JavaScript中通常称它们为强制类型转换。其实我认为更确切地说可以分为"显示强制类型转换"和"隐式强制类型转换"。
数据类型
JS中有六种简单的数据类型,undefined,null,string,number,symbol,以及一种复杂类型object,但是JavaScript在声明时只有一种类型,只有到运行的期间才会确定当前类型,在运行的时候,由于JavaScript是弱类型语言,没有对类型做严格的限制,导致不同类型之间可以进行运算,这样就不得不涉及类型之间相互转换了。
显示强制类型转换
显示类型转换就是手动地将一种值转换为另一种值,是指代码有明确意图的转换。
显示转化主要是指使用 Number(),String(),Boolean() 三个函数,手动将各种类型的值,分别转为数字、字符串或者布尔值,但是要注意,它们前面是没有new的,并不会封装对象。而上述的三个函数的实现原理,就是使用抽象操作,分别调用 ToNumber(),ToString(),ToBoolean() 来实现。
除了 Number(),String(),Boolean() 三个函数外,一下的方法也是显示转换。
参考ECMA官方文档
1. toString()
toString负责处理非字符串强制转成字符串,规则如下:
// 数字转字符串
(123).toString() // '123'
// 布尔值转字符串
(true).toString() // 'true'
// 数组转字符串
['hello', 'world'].toString() // 'hello,world'
// 对象转字符串
({name: 'hello world'}).toString() // '[object Object]'
//日期对象转字符串
Date().toString() // 'Sat Aug 08 2020 01:26:31 GMT+0800 '
//JSON对象转字符串
JSON.toString() // '[object JSON]'
// Function转字符串
Function.toString() // 'function Function() { [native code] }'
// 函数转字符串
(function(){ return 1; }).toString() // 'function () { return 1; }'
普通对象来说,调用的是内部的toPrimirive方法实现,除非自行定义,否则用Object.prototype.toString()返回内部属性[[class]]的值,如[object object]。
2.toNumber
其他类型转换到 number 类型的规则见下方表格:
String转为Number的规则:
- (1)如果字符串中包含数字那么久转成对一个的数字
- (2)如果字符串中包含十六进制格式,那么转成对一个十进制的数字
- (3)如果字符串为空,那么转换成0
- (4)如果字符串包含上述之外的字符,那么转化为NaN。
3.toBoolean
toBoolean转换规则如下:
有上面的规则可见,所有的对象都返回true,包括[]和{}。 但这也涉及到一个问题,就是假值对象(falsy object)
var a = new Boolean(false) //false
var b = new Number(0) //false
var c = new String('') //false
Boolean(a && b && c) //true 注意此处必须用Boolean来封装
所以false值被封装后返回true。
4.一元运算符(+ -)
+和 - 0 能显式地将非数字转换为数字。
console.log(+ '1.02'); //1.02
console.log(+ -0); //-0
console.log(+ []); //0
console.log(+ {}); //NaN
console.log({} -0); //NaN
5.parseInt()
处理整数的时候会经常用到parseInt()。parseInt()函数在转换字符串时,会忽略字符串前面的空格,直到找到第一个非空格字符。
如果第一个字符不是数字或者负号,parseInt() 就会返回NaN,同样的,用parseInt() 转换空字符串也会返回NaN。
如果第一个字符是数字字符,parseInt() 会继续解析第二个字符,直到解析完所有字符串或者遇到了一个非数字字符,只返回数字字符部分。
parseInt()方法还有基模式,可以把二进制、八进制、十六进制或其他任何进制的字符串转换成整数。
基是由parseInt()方法的第二个参数指定的,所以要解析十六进制的值,当然,对二进制、八进制,甚至十进制(默认模式),都可以这样调用parseInt()方法。
如果第二个参数是 0、undefined 和 null,会直接忽略。
console.log( parseInt(' -88ddd2 ') ); //-88
console.log(parseInt("100",2)); //4
console.log(parseInt("AF")); //NaN
console.log(parseInt("AF",16)); //175
console.log(parseInt("AG",16)); // 10 只转16进制部分
console.log(parseInt("1.23")); // 1 这里会先将1.23转化为字符串
console.log(( parseInt('90',undefined) )); //90
6.parseFloat
与parseInt() 函数类似,parseFloat() 也是从第一个字符(位置0)开始解析每一个字符。也是一直解析到字符串末尾,或者解析到遇见一个无效的浮点数字字符为止。
也就是说,字符串中第一个小数点是有效的,而第二个小数点就是无效的了,它后面的字符串将被忽略。
但是parseFloat() 只解析十进制,所以它不需要指定第二个参数作作为基数。
如果字符串中包含的是一个可解析为正数的数(没有小数点,或者小数点后都是零),parseFloat() 会返回整数。
console.log(parseFloat('123AF')); //123
console.log(parseFloat("22.3.56")); //22.3
console.log(parseFloat("04.900")); //4.9
parseInt() 和parseFloat() 的区别在于:
- parseFloat() 所解析的字符串中第一个小数点是有效的,而parseInt() 遇到小数点会停止解析,因为小数点并不是有效的数字字符。
- parseFloat() 始终会忽略前导的零,十六进制格式的字符串始终会被转换成0,而- parseInt() 第二个参数可以设置基数,按照这个基数的进制来转换。
隐式强制类型转换
隐式类型转换一般是在涉及到运算符的时候才会出现的情况,比如我们将两个变量相加,或者比较两个变量是否相等。 隐式类型转换其实在我们上面的例子中已经有所体现。对于对象转原始类型的转换,也会遵守 ToPrimitive 的规则,下面会进行细说。
1.从ES规范来看类型转换
ToPrimitive
在对象转原始类型的时候,一般会调用内置的 ToPrimitive 方法,而 ToPrimitive 方法则会调用 OrdinaryToPrimitive 方法.
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。
(1)如果输入的值是个对象
(2)如果 hint 是个字符串并且值为'string'或者'number'
(3)如果 hint 是'string',那么就将 methodNames 设置为 toString、valueOf
(4)如果 hint 是'number',那么就将 methodNames 设置为 valueOf、toString
(5)遍历 methodNames 拿到当前循环中的值 name,将 method 设置为 O[name](即拿到 valueOf 和 toString 两个方法)
(6)如果 method 可以被调用,那么就让 result 等于 method 执行后的结果,如果 result 不是对象就返回 result,否则就抛出一个类型错误的报错。
ToPrimitive 的代码实现
如果只用文字来描述,你肯定会觉得过于晦涩难懂,所以这里我就自己用代码来实现这两个方法帮助你的理解。
// 获取类型
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 方法,如果执行这两个方法后得到了原始类型的值,那么就返回。否则,将会抛出错误。
Symbol.toPrimitive
在 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'
总结 类型转换一直是学 JS 的时候很难搞明白的一个概念,因为转换规则比较复杂,经常让人觉得莫名其妙。 但是如果从 ECMA 的规范去理解这些转换规则的原理,那么就会很容易知道为什么最后会得到那些结果。