JavaScript中的数据类型转换

JavaScript中的数据类型转换是一个很容易造成困惑的地方。本文是《你不知道的JavaScript(中)》类型转换相关的知识整理,强烈建议去看这本书。

<!--more-->

首先我们需要了解JS中一些显式的类型转换

1. 显式类型转换

1.1. ToString

ToString负责非字符串到字符串的强制类型转换

对于基础类型

  • null转换为'null'
  • undefined转换为'undefined'
  • true转换为'true',false转换为'false'
  • 数字遵循通用规则转换,极大值或极小值使用指数形式

对于对象而言,

  • 如果未覆盖toString,则返回[object Object]
  • 如果覆盖了toString,则调用该方法并使用返回值
  • 数组对象默认自定义了toString,会将所有单元字符串化之后再使用逗号,连接起来
    • 这里的一个特例是null与undefined的元素会被字符串化为空字符串''而不是上面的普通类型转换规则

1.2. ToPrimitive

关于对象转原始类型ToPrimitive(input, PreferredType?),可以参考:

其中hint的取值可以为numberstringdefault,表示当前语境传入的值(或者说是表示这是一个什么类型的运算),常见的运算中

var a = {
    [Symbol.toPrimitive](hint) {
        console.log(hint)
    }
}
+a // hint为number
a + 1 // hint为default

// 下面情况hint为string
var o = {}
o[a] = 'xx' // string
alert(a) // string
parseInt(a) // string

ToPrimitive的具体流程如下:

  • 如果对象存在Symbol.toPrimitive,调用 objSymbol.toPrimitive
    • 其中hint为当前语境传入的值(或者说是表示这是一个什么类型的运算),取numberstringdefault
  • 如果不存在Symbol.toPrimitive,且 hint 取值是 "string":
    • 无论是否存在,先调用 obj.toString(),如果返回了原始值,则使用该值;否则调用obj.valueOf()
  • 否则(也就是 hint 取值是 "number" 或 "default" 的情况):
    • 无论是否存在,先调用 obj.valueOf(),如果返回了原始值,则使用该值;否则调用obj.toString()

需要注意

  • Symbol.toPrimitive方法或最后调用的obj.toString只能返回原始值,否则会抛出TypeError
  • 我们创建的字面量对象{}[]不包含Symbol.toPrimitive方法,且valueOf方法返回的是自身,仍旧为引用值
  • 但是Date对象实现了默认的Symbol.toPrimitive方法,且返回的返回的是date.toString的值
let user = {
    name: "John",
    money: 1000,

    [Symbol.toPrimitive](hint) {
        // 手动修改toPrimitive的值
        return false
    }
};
console.log(user == false) // true

// 先调用valueOf,返回[],需要再调用toString,返回'', 表达式变成'' == false,
// 然后将布尔值转换为数字, '' == 0, 
// 字符串与数字比较时将字符串转换为数字,表达式变为 0 == 0,返回true
console.log([] == false) 

总结一下,toPrimitive将对象转换为基础值

  • 首先检查该值是否有valueOf方法,如果有且该方法返回的是基础值,则使用该返回值进行强制类型转换
    • 注意数组的valueOf返回自身
  • 如果没有,则使用toString的返回值进行强制类型转换
  • 如果不具备valueOftoString则报TypeError

1.3. ToNumber

ToNumber定义了将非数字转换为数字的规则

基本类型的转换

  • true转换为1,false转换为0
  • undefined转换为NaNnull转换为0
  • '''\n'' '等空白字符时会返回0,其余数字常量的字符串会转换为对应数字,转换失败则返回NaN
    • '0'开头的十六进制数字字符串是按照十进制进行转换

对象首先会被ToPrimitive转换成对应的基本值,如果返回的是非数字的基本类型,再应用上面的规则转换为数字

1.4. ToBoolean

JS中的值分为两类,

  • 可以被强制转换为false的值,包括undefinednullfalse+0-0NaN和空字符串''
  • 其他被强制转换为true的值
    • 注意对象(包括包装对象)永远为真值

2. 隐式类型转换

=====的区别大家都比较了解,正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”

如果两个值的类型相同,则仅比较他们是否相等,但是要注意特例

  • NaN不等于NaN
  • +0等于-0
  • 引用类型判断的是内存地址是否相等,而不是具体值是否相等

如果两个值的类型不同,则会发生强制类型转换,会将其中之一或者二者转换成同一类型后再继续比较

x == y时 ES5 规范 11.9.3.4-5 这样定义:

规则1:二者有一个为数字,另一个为字符串,则将字符串转换成数字类型

  • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
  • 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果
    // 返回100 == 100的结果,类型相同,比较值,返回true
    '100' == 100 // true 
    

// 首先ToNumber('100x')返回NaN,然后 NaN == 100都为数字类型,比较值,返回false '100x' == 100 // false


规则2:二者有一个为布尔值,另一个为其他类型时,会先将布尔值转换成数字,然后再进行`==`比较   
* 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果
* 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果
* `ToNumber(true)`的结果为1`ToNumber(false)`的结果为false
```js
// 首先将a转换成数字1,此时计算 1 == '100',应用上面的规则1,将字符串转换成数字,返回 1 == 100的结果
true == '100' // false 

// 基于上面的特点,永远不要使用下面的写法
if(a == true) {}

规则3:null与undefined之间的比较

  • 如果 x 为 null,y 为 undefined,则结果为 true。
  • 如果 x 为 undefined,y 为 null,则结果为 true。
  • 除此之外其他任何值都与这两个值不相等
    null == undefined // true
    undefined = true // true
    

null == true // false null == false // false null == '' // false null == 0 // false

undefined == true // false undefined == false // false undefined == '' // false undefined == 0 // false


规则4:对象与非对象之间的对比
* 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果; 
* 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果。
* 如果基础值是布尔值,会先应用规则3,然后再进行上面的ToPrimitive操作

// 首先[42]调用ToPrimitive转换为基础值 42 == [42] // true


建议
* 如果两边的值中有 true 或者 false,千万不要使用 ==。 
* 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==。


还有一些常见的坑
```js

// []转换为'',而{}转换为[object Object],因此这里是字符串相加
[] + {}; // "[object Object]" 

// 这里{}会被当做一个空的代码块,因此等价于 +[],会将[]转换为数字,
// 因此调用ToPrimitive返回空字符串, 然后将空字符串转ToNumber换为数字0
{} + []; // 0

3. 小结

JavaScript的数据类型转换是开发和面试中经常遇见的问题,稍有不慎就容易犯错,本文主要整理类型转换的一些规则,用以备忘。

理解EventLoop如何部署前端代码