彻底搞懂 JavaScript 中头疼的数据类型转换

279 阅读14分钟

这是我参与8月更文挑战的第7天,活动详情查看8月更文挑战

前言

JavaScript 中的类型转换一直都是让前端开发者最头疼的问题。看网上有张图,说 JavaScript 让人不可思议。

我们来看看那张图,看输出结果是否为意料之中的结果

image.png

因为 JavaScript 本身是一门弱类型语言,以至于类型转换发生的频率很高,本文将帮助大家梳理各种类型之间的相互转换,当然你看完整篇文章,肯定知道为什么执行上面的代码,输出那样的结果。下面,我们就开始深入理解 JavaScript 中的类型转换,让类型转换不再成为前端开发中的拦路虎

JavaScript 类型转换分类

类型转换分为两种:显式类型转换、隐式类型转换。

显式类型转换:就是我们得动手操作,将一种值转换为另一种值

隐式类型转换:不用人为操作,程序会偷偷的进行类型转换

一般来说,类型转换主要是基本类型转基本类型、引用类型转基本类型,转换的目标类型主要分为以下几种:

  • 转换为 number
  • 转换为 string
  • 转换为 boolean

下面会分别一一说明:

显式/强制类型转换

ToNumber:转换为 number

其他类型转换为 number 类型的规则,见下方表格

原始值转换结果
string根据语法和转换规则来转换
true1
false0
null0
undefinedNaN
symbolTypeError: Cannot convert a Symbol value to a number
object先调用 toPrimitive,再调用 toNumber

注:

  • 根据规范,如果 Number 函数不传参数,返回 +0,如果有参数,调用 ToNumber(value)

注意这个 ToNumber 表示的是一个底层规范实现上的方法,并没有直接暴露出来。

我们可以使用 + 将其他类型转为 number 类型,验证一下上面的表格的转换结果

console.log(+true); // 1
console.log(+false); // 0
console.log(+null); // 0
console.log(+undefined); // NaN
console.log(+Symbol('hi')); // Uncaught TypeError: Cannot convert a Symbol value to a number
// 前言图片的那个代码,输出的结果
console.log('b' + 'a' + + 'a' + 'a'); 
// "baNaNa",然后调用字符串的方法,让字符串变成小写,结果是 banana
console.log( ('b' + 'a' + + 'a' + 'a').toLowerCase() ); // "banana"

string 类型的值,转换为 number 类型的规则: 其实本质就是

  1. 如果字符串中只包含数字,那么就转换为对应的数字。
  2. 如果字符串中只包含十六进制格式,那么就转换为对应的十进制数字
  3. 如果字符串为空或者只包含空格,那么转换为 0
  4. 如果字符串包含上述之外的字符串,那么转换为 NaN。
// 第 1 点
console.log(+'123'); // 123
// 第 2 点
console.log(+'0x100f'); // 4111
// 第 3 点
console.log(+''); // 0
// 第 4 点
console.log(+'hi 234'); // NaN

ToString:转换为 string

原始值转换结果
number对应的字符串类型
true'true'
false'false'
null'null'
undefined'undefined'
symbolTypeError: Cannot convert a Symbol value to a string
object先调用 toPrimitive,再调用 toString

我们可以使用 模板字符串 将其他类型转为 string 类型,验证一下上面的规律:

console.log(`${11}`); // "11"
console.log(`${true}`); // "true"
console.log(`${false}`); // "false"
console.log(`${null}`); // "null"
console.log(`${undefined}`); // "undefined"
console.log(`${Symbol('hi')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string
console.log(`${{}}`); // "[object Object]"

ToBoolean:转换为 boolean

记住规律 5 个falsy

在JS当中只有"0 / NaN / 空字符串'' "" / null / undefined"这个5个值转换为布尔类型的 false,其余都转换为 true 注:

  • 当 Boolean 函数不传任何参数时,会返回 false
  • false 本身就代表布尔值

我们可以使用 !! 将其他类型转为 boolean 类型,验证一下上面的规律:

// !! 两次取反,等价于没取反,相当于转成 boolean 类型
console.log(!!0); // false
console.log(!!NaN); // false
console.log(!!''); // false
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!Symbol()); // true
console.log(!!{}); // true

来来来,看下大家是否掌握了规律,下面代码的输出结果是?

console.log(!!false); // false
console.log(!!new Boolean(false)); // true

为什么第二个的输出结果是 true? 其实很简单,就是因为执行 new Boolean(false),相当于是创建了一个其值为false的布尔对象,所以它不是 5 个 falsy 值里面,所以结果是 true。

隐式类型转换

下面我们看看发生隐式类型转换的几个场景,以及如何转换的。

数学算符中的类型转换

减、乘、除

我们在对各种非number类型运用数学运算符(- * /)时,会先将非number类型转换为number类型;

1 - true // 0,首先把 true 转换为数字 1, 然后执行 1 - 1
1 - null // 1, 首先把 null 转换为数字 0, 然后执行 1 - 0
1 * undefined //  NaN, undefined 转换为数字是 NaN
2 * ['5'] //  10, ['5']首先会变成 '5', 然后再变成数字 5

上面例子中的 ['5'] 的转换,涉及到拆箱操作

特殊的加法

加法,比较特殊,需要做下区分。

因为 + 不仅仅是数学运算,还可以用来拼接字符串。所以需要记住下面 3 条规则:

  1. 当一侧为string类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。
  2. 当一侧为number类型,另一侧为原始类型,则将原始类型转换为number类型。
  3. 当一侧为number类型,另一侧为引用类型,将引用类型和number类型转换成字符串后拼接。

以上 3 点,优先级从高到低。

// 规则1
123 + '123' // "123123"

// 规则2
123 + null  // 123
123 + true // 124

// 规则3
123 + {}  // "123[object Object]"

if 语句和逻辑语句

if语句和逻辑语句中,如果只有单个变量,会先将变量转换为Boolean值,5 个 falsy 值和 false 会转换成 false,其余都转换为 true 记住规律 5 个falsy

在JS当中只有"0 / NaN / 空字符串'' "" / null / undefined"这个5个值转换为布尔类型的 false,其余都转换为 true

==

日常开发中,我们一般都是使用 === 来判断,不过了解一下 == 特点就好,因为感觉太难记了。

使用==时,若两侧类型相同,则比较结果和===相同,否则会发生隐式转换,使用==时发生的转换可以分为几种不同的情况(只考虑两侧类型不同):

情况 1

NaN和其他任何类型比较永远返回false(包括和他自己)。

NaN == NaN // false
情况 2

boolean和其他任何类型比较,boolean首先被转换为number类型。

true == 1  // true 
true == '2'  // false, 先把 true 变成 1,而不是把 '2' 变成 true
true == ['1']  // true, 先把 true 变成 1, ['1']拆箱成 '1', 再参考情况3
true == ['2']  // false, 同上
undefined == false // false ,首先 false 变成 0,然后参考情况4
null == false // false,同上
情况 3

stringnumber比较,先将string转换为number类型

123 == '123' // true, '123' 会先变成 123
'' == 0 // true, '' 会首先变成 0
情况 4

null == undefined比较结果是true,除此之外,null、undefined和其他任何结果的比较值都为false

null == undefined // true
null == '' // false
undefined == '' // false
情况 5

当原始类型和引用类型做比较时,对象类型会依照ToPrimitive规则转换为原始类型:

ToPrimitive规则,是引用类型向原始类型转变的规则,它遵循先valueOftoString的模式期望得到一个原始类型。如果还是没法得到一个原始类型,就会抛出 TypeError

'[object Object]' == {} 
// true, 对象和字符串比较,对象通过 toString 得到一个基本类型值
'1,2,3' == [1, 2, 3] 
// true, 同上  [1, 2, 3]通过 toString 得到一个基本类型值

[] == ![] // true
// `!`的优先级高于`==`,`![]`首先会被转换为`false`,然后根据上面第二点,
// `false`转换成`Number`类型`0`,左侧`[]`转换为`0`,两侧比较相等。

[null] == false // true
[undefined] == false // true
// 根据数组的ToPrimitive规则,数组元素为null或undefined时,
// 该元素被当做空字符串处理,所以[null]、[undefined]都会被转换为0。

终于写完 == 了,太难用了。

image.png

下面是 x == y 的「真值表」,左边表示 x 的取值,上边表示 y 的取值,绿色表示真,白色表示假:

image.png

== 是一个难用的符号,非常非常容易出错!!

所以,我们推荐使用 === 代替 ==。

x === y 的真值表:

image.png

从 ES 规范来看类型转换

整篇文章,ToPrimitive 这个单词,出现的次数比较多。接下来,我们就来从 ES 规范来看类型转换。

先了解下 toString 和 valueOf

toString

toString() 方法返回一个表示该对象的字符串。

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

值得注意的是,上面提到了如果此方法在自定义对象中未被覆盖,toString 才会达到预想的效果,事实上,大部分引用类型比如 Array、Date、RegExp 等都重写了 toString 方法。

console.log(({}).toString()) // "[object Object]"

console.log([].toString()) // ""
console.log([0].toString()) // "0"
console.log([1, 2, 3].toString()) // "1,2,3"
console.log((function(){var a = 1;}).toString()) // "function(){var a = 1;}"
console.log((/\d+/g).toString()) // "/\\d+/g"
console.log((new Date(2010, 0, 1)).toString()) // "Fri Jan 01 2010 00:00:00 GMT+0800 (中国标准时间)"

valueOf

valueOf() 方法返回指定对象的原始值。

默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。

var date = new Date(2021, 8, 14);
console.log(date.valueOf()); // 1631548800000

对象到字符串是如何转换的

参数类型结果
Object1. primValue = ToPrimitive(input, String)
2. 返回ToString(primValue).

所谓的 ToPrimitive 方法,其实就是输入一个值,然后返回一个一定是基本类型的值。

小结

当我们用 String 方法转化一个值的时候

  • 如果是基本类型,就"ToString:转换为 string" 对应的表格
  • 如果不是基本类型,我们会调用 ToPrimitive 方法,将其转为基本类型,然后按照上面基本类型的情况,进行转换

对象到数字是如何转换的

参数类型结果
Object1. primValue = ToPrimitive(input, Number)
2. 返回ToNumber(primValue)。

小结

当我们用 Number 方法转化一个值的时候

  • 如果是基本类型,就"ToNumber:转换为 number" 对应的表格
  • 如果不是基本类型,我们会调用 ToPrimitive 方法,将其转为基本类型,然后按照上面基本类型的情况,进行转换

虽然转换成基本值都会使用 ToPrimitive 方法,但传参有不同,最后的处理也有不同,转字符串调用的是 ToString,转数字调用 ToNumber

ToPrimitive

上面了解到对象是如何转换为字符串和数字,都会使用 ToPrimitive 方法,但传参有不同,最后的处理也有不同,转字符串调用的是 ToString,转数字调用 ToNumber

我们先来看看 ES5 规范 9.1 函数语法表示如下:

ToPrimitive(input[, PreferredType])

image.png

第一个参数是 input,表示要处理的输入值。

第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String。

当不传入 PreferredType 时,如果 input 是日期类型,相当于传入 String,否则,都相当于传入 Number。

如果传入的 input 是 Undefined、Null、Boolean、Number、String 类型,直接返回该值。

如果是 ToPrimitive(obj, Number),处理步骤如下:

  1. 如果 obj 为 基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

如果是 ToPrimitive(obj, String),处理步骤如下:

  1. 如果 obj为 基本类型,直接返回
  2. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

小结

引用类型转换为 number 类型

其实就是Number 函数背后的转换规则

第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。

第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。

第三步,如果toString方法返回的是对象,就报错。

引用类型转换为 string 类型

其实就是 String 函数背后的转换规则,与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序。

第一步,先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

第二步,如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

第三步,如果valueOf方法返回的是对象,就报错。

结束了吗?还没呢,还可以继续往下深入了解,大家可以根据实际情况,决定是否继续往下哈。

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

image.png

大佬的翻译

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) 抽象操作的结果

大白话来理解的话,在进行类型转换的时候,一般是通过 toPrimitive 方法,将引用类型转换为基本类型。如果引用类型上有 @@toPrimitive 方法,就调用 @@toPrimitive 方法,相当于执行上面说的 exoticToPrim,执行的返回值为原始值,则直接返回,否则,就抛出错误异常。

OrdinaryToPrimitive

image.png

而 OrdinaryToPrimitive 方法也接受两个参数,一个是输入的值 O,一个也是期望转换的类型 hint

  1. 如果输入的值是个对象
  2. 如果 hint 是个字符串并且值为'string'或者'number'
  3. 如果 hint 是'string',那么就将 methodNames 设置为 toStringvalueOf
  4. 如果 hint 是'number',那么就将 methodNames 设置为 valueOftoString
  5. 遍历 methodNames 拿到当前循环中的值 name,将 method 设置为 O[name](即拿到 valueOf 和 toString 两个方法)
  6. 如果 method 可以被调用,那么就让 result 等于 method 执行后的结果,如果 result 不是对象就返回 result,否则就抛出一个类型错误的报错。

大白话来理解的话,调用 OrdinaryToPrimitive 方法的时候,需要判断期望转换的目标类型,决定是先调用 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'

参考

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。