[关闭]
@gyyin 2019-10-03T14:12:13.000000Z 字数 5593 阅读 300

类型转换知多少

慕课专栏


前言

JavaScript 中的类型转换一直都是让前端开发者最头疼的问题。前阵子,推特上有个人专门发了一张图说 JavaScript 让人不可思议。

image_1dm5s9qr814dvnsi96laugvg9.png-51.4kB

除了这个,还有很多经典的、让 JavaScript 开发者摸不着头脑的类型转换,譬如下面这些,你是否知道结果都是多少?

  1. 1 + {} === ?
  2. {} + 1 === ?
  3. 1 + [] === ?
  4. 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。
一般来说,类型转换主要是基本类型转基本类型、复杂类型转基本类型两种。
转换的目标类型主要分为以下几种:

  1. 转换为string
  2. 转换为number
  3. 转换为boolean

我参考了ECMA262的官方文档来总结一下这几种类型转换。ECMA 文档链接:ECMA-262

ToNumber

其他类型转换到number类型的规则见下方表格:

原始值 转换结果
Undefined NaN
Null 0
true 1
false 0
String 根据语法和转换规则来转换
Symbol Throw a TypeError exception
Object 先调用toPrimitive,再调用toNumber

String转换为Number类型的规则:

  1. 如果字符串中只包含数字,那么就转换为对应的数字。
  2. 如果字符串中只包含十六进制格式,那么就转换为对应的十进制数字。
  3. 如果字符串为空,那么转换为0。
  4. 如果字符串包含上述之外的字符,那么转换为NaN。

使用+可以将其他类型转为number类型,我们用下面的例子来验证一下。

  1. +undefined // NaN
  2. +null // 0
  3. +true // 1
  4. +false // 0
  5. +'111' // 111
  6. +'0x100F' // 4111
  7. +'' // 0
  8. 'b' + 'a' + + 'a' + 'a' // 'baNaNa'
  9. +Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number

ToBoolean

原始值 转换结果
Undefined false
Boolean true or false
Number 0和NaN返回false,其他返回true
Symbol true
Object true

我们也可以使用Boolean构造函数来手动将其他类型转为boolean类型。

  1. Boolean(undefined) // false
  2. Boolean(1) // true
  3. Boolean(0) // false
  4. Boolean(NaN) // false
  5. Boolean(Symbol()) // true
  6. Boolean({}) // true

ToString

原始值 转换结果
Undefined 'Undefined'
Boolean 'true' or 'false'
Number 对应的字符串类型
String String
Symbol Throw a TypeError exception
Object 先调用toPrimitive,再调用toNumber

转换到string类型同样可以用构造函数String来实现。

  1. String(undefined) // 'undefined'
  2. String(true) // 'true'
  3. String(false) // 'false'
  4. String(11) // '11'
  5. String(Symbol()) // 'Symbol()'
  6. String({})

隐式类型转换

隐式类型转换一般是在涉及到运算符的时候才会出现的情况,比如我们将两个变量相加,或者比较两个变量是否相等。
隐式类型转换其实在我们上面的例子中已经有所体现。对于对象转原始类型的转换,也会遵守ToPrimitive的规则,下面会进行细说。

从ES规范来看类型转换

ToPrimitive

在对象转原始类型的时候,一般会调用内置的ToPrimitive方法,而ToPrimitive方法则会调用OrdinaryToPrimitive方法,我们可以看一下ECMA的官方文档。

image_1dard6av87ir24p140nv5d1vq9.png-182.5kB

我来翻译一下这段话。

ToPrimitive方法接受两个参数,一个是输入的值input,一个是期望转换的类型PreferredType。

  1. 如果没有传入PreferredType参数,让hint等于"default"
  2. 如果PreferredType是hint String,让hint等于"string"
  3. 如果PreferredType是hint Number,让hint等于"number"
  4. 让exoticToPrim等于GetMethod(input, @@toPrimitive),意思就是获取参数input的@@toPrimitive方法
  5. 如果exoticToPrim不是Undefined,那么就让result等于Call(exoticToPrim, input, « hint »),意思就是执行exoticToPrim(hint),如果执行后的结果result是原始数据类型,返回result,否则就抛出类型错误的异常
  6. 如果hint是"default",让hint等于"number"
  7. 返回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的代码实现

如果只用文字来描述,你肯定会觉得过于晦涩难懂,所以这里我就自己用代码来实现这两个方法帮助你的理解。

  1. // 获取类型
  2. const getType = (obj) => {
  3. return Object.prototype.toString.call(obj).slice(8,-1);
  4. }
  5. // 是否为原始类型
  6. const isPrimitive = (obj) => {
  7. const types = ['String','Undefined','Null','Boolean','Number'];
  8. return types.indexOf(getType(obj)) !== -1;
  9. }
  10. const ToPrimitive = (input, preferredType) => {
  11. // 如果input是原始类型,那么不需要转换,直接返回
  12. if (isPrimitive(input)) {
  13. return input;
  14. }
  15. let hint = '',
  16. exoticToPrim = null,
  17. methodNames = [];
  18. // 当没有提供可选参数preferredType的时候,hint会默认为"default";
  19. if (!preferredType) {
  20. hint = 'default'
  21. } else if (preferredType === 'string') {
  22. hint = 'string'
  23. } else if (preferredType === 'number') {
  24. hint = 'number'
  25. }
  26. exoticToPrim = input.@@toPrimitive;
  27. // 如果有toPrimitive方法
  28. if (exoticToPrim) {
  29. // 如果exoticToPrim执行后返回的是原始类型
  30. if (typeof (result = exoticToPrim.call(O, hint)) !== 'object') {
  31. return result;
  32. // 如果exoticToPrim执行后返回的是object类型
  33. } else {
  34. throw new TypeError('TypeError exception')
  35. }
  36. }
  37. // 这里给了默认hint值为number,Symbol和Date通过定义@@toPrimitive方法来修改默认值
  38. if (hint === 'default') {
  39. hint = 'number'
  40. }
  41. return OrdinaryToPrimitive(input, hint)
  42. }
  43. const OrdinaryToPrimitive = (O, hint) => {
  44. let methodNames = null,
  45. result = null;
  46. if (typeof O !== 'object') {
  47. return;
  48. }
  49. // 这里决定了先调用toString还是valueOf
  50. if (hint === 'string') {
  51. methodNames = [input.toString, input.valueOf]
  52. } else {
  53. methodNames = [input.valueOf, input.toString]
  54. }
  55. for (let name in methodNames) {
  56. if (O[name]) {
  57. result = O[name]()
  58. if (typeof result !== 'object') {
  59. return result
  60. }
  61. }
  62. }
  63. throw new TypeError('TypeError exception')
  64. }

总结一下,对象通过ToPrimitive方法转为原始类型,如果对象上有toPrimitive方法,就调用toPrimitive方法,执行后返回值为原始类型就直接返回,如果依然是对象,那么就会报错。
如果对象上没有toPrimitive方法,那么就根据转换的目标类型来判断先调用toString还是valueOf方法,最后返回结果。

Symbol.toPrimitive

在ES6之后提供了Symbol.toPrimitive方法,该方法在类型转换的时候优先级最高。

  1. const obj = {
  2. toString() {
  3. return '1111'
  4. },
  5. valueOf() {
  6. return 222
  7. },
  8. [Symbol.toPrimitive]() {
  9. return 666
  10. }
  11. }
  12. const num = 1 + obj; // 667
  13. const str = '1' + obj; // '1666'

例子

也许上面关于ToPrimitive的代码讲解你还是会觉得晦涩难懂,那我接下来就举几个例子来说明对象的类型转换。

  1. var a = 1,
  2. b = '2';
  3. var c = a + b; // '12'

也许你会好奇,为什么不是将后面的b转换为number类型,最后得到3?
我们还是要先看文档对加号的定义。

image_1davvk6ij3lnsisjsk1i8djf8p.png-243.3kB

首先会分别执行两个值的toPrimitive方法,因为a和b都是原始类型,所以还是得到了1和'2'。
从图上看到如果转换后的两个值的Type有一个是String类型,那么就将两个值经过toString转换后串起来。因此最后得到了'12',而不是3。

我们还可以再看一个例子。

  1. var a = 'hello ', b = {};
  2. 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方法就好了。

  1. b.valueOf = function() {
  2. return 'world'
  3. }
  4. var c = a + b; // 'hello world'

也许你在面试题中看到过这个例子。

  1. var a = [], b = [];
  2. var c = a + b; // ''

这里为什么c最后是''呢?因为a和b在执行valueOf之后,得到的依然是个[],这并非原始类型,因此会继续执行toString,最后得到'',两个''相加又得到了''。
我们再看一个指定了preferredType的例子。

  1. var a = [1, 2, 3], b = {
  2. [a]: 111
  3. }

由于a是作为了b的键值,所以preferredType为string,这时会调用a.toString方法,最后得到了'1,2,3'

总结

类型转换一直是学JS的时候很难搞明白的一个概念,因为转换规则比较复杂,经常让人觉得莫名其妙。
但是如果从ECMA的规范去理解这些转换规则的原理,那么就会很容易知道为什么最后会得到那些结果。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注