再深入理解一次JavaScript中数据类型的转换

259 阅读8分钟

一般来说,类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时,然而在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 的规范去理解这些转换规则的原理,那么就会很容易知道为什么最后会得到那些结果。


参考:juejin.cn/post/684490… zhuanlan.zhihu.com/p/85731460