引言

在 JavaScript 中,类型转换是一个关键的概念,它影响着代码的行为和结果。理解如何在不同上下文中进行显式和隐式的类型转换不仅可以帮助你编写更可靠的代码,还能在面试中展示你的深入理解。在这篇文章中,我们将详细探讨 JavaScript 中的类型转换规则,包括显式类型转换、对象到原始值的转换,以及运算符中的类型转换。

js中的类型转换机制是什么?

js中有七种原始数据类型:Number、String、Boolean、undefined、null、 Symbol (ES6 引入)、BigInt(ES11 引入),和引用数据类型:Object、Array、Function、Date、正则表达式(RegExp)、Map 和 Set(ES6 引入)、WeakMap 和 WeakSet(ES6 引入)等,在开发时有时候我们需要人为的将一个变量的类型转为其他类型,这种转换称之为显示转化,有时候js引擎也会发生隐式转换。

显式类型转换

原始值转布尔 Boolean()

Boolean() 函数将任何值转换为布尔值。根据 JavaScript 的规则,以下原始值会被转换为 false

  • false
  • 0-0
  • NaN
  • ""(空字符串)
  • null
  • undefined

所有其他值(包括对象和非空字符串)都会被转换为 true。例如:

console.log(Boolean(0));         // false
console.log(Boolean('Hello'));   // true
console.log(Boolean([]));        // true

原始值转数字 Number()

Number() 函数将其他类型的值转换为数字。常见转换包括:

  • 字符串:若字符串包含有效数字,则转换为对应的数字,否则转换为 NaN
  • 布尔值:true 转换为 1false 转换为 0
  • 对象:先调用 valueOf() 方法,如果返回原始值,则转换为该值;否则,调用 toString() 方法。
console.log(Number('123'));      // 123
console.log(Number('123abc'));  // NaN
console.log(Number(true));      // 1
console.log(Number(false));     // 0
console.log(Number(null))       // 0
console.log(Number(undefined))  // NaN

原始值转字符串 String()

String() 函数将其他类型的值转换为字符串:

  • 数字和布尔值会转换为其对应的字符串。
  • 对象:先调用 toString() 方法,如果返回原始值,则转换为该值;否则,调用 valueOf() 方法。
console.log(String(123));       // "123"
console.log(String(true));      // "true"
console.log(String([]));        // ""
console.log(String({}));        // "[object Object]"

原始值转对象 new Xxx()

使用构造函数 new Number(), new String(), 或 new Boolean() 创建包装对象。这些对象可以调用原始值的方法,但它们本质上是对象,而不是原始值。

let numObj = new Number(123);
console.log(numObj.valueOf());  // 123
console.log(numObj.toString()); // "123"

隐式类型转换

对象转原始值

JavaScript 在特定情况下会将对象隐式转换为原始值。对象的转换过程遵循以下规则:

  • 调用 Object.prototype.toString() :所有对象(包括数组和普通对象)会调用 toString() 方法,通常返回 [object Object][object Array] 等字符串。例如:
console.log({}.toString());   // "[object Object]"
console.log([].toString());   // ""
  • 调用 valueOf()valueOf() 方法用于返回对象的原始值。对于包装对象(如 NumberStringBoolean),valueOf() 返回相应的原始值。例如:
let numObj = new Number(123);
console.log(numObj.valueOf()); // 123

ToPrimitive 抽象操作

ToPrimitive 是一个抽象操作,用于将对象转换为原始值,按照以下步骤进行:

  • ToPrimitive(obj, String) => String(obj) :先调用 toString(),如果返回原始值,则使用该值;否则调用 valueOf(),如仍未得到原始值,则抛出 TypeError
let obj = {
    toString() { return 'string'; },
    valueOf() { return 42; }
};
console.log(String(obj)); // "string"
  • ToPrimitive(obj, Number) => String(obj) :先调用 valueOf(),如果返回原始值,则使用该值;否则调用 toString(),如仍未得到原始值,则抛出 TypeError
let obj = {
    toString() { return 'string'; },
    valueOf() { return 42; }
};
console.log(Number(obj)); // 42
  • 默认顺序:通常情况下,ToPrimitive 转换规则会优先调用 valueOf() 方法。如果 valueOf() 不返回原始值,则调用 toString() 方法。
  • 字符串上下文:在需要将对象转换为字符串的上下文中(如字符串拼接),toString() 方法会被优先调用。

一元运算符和二元运算符中的类型转换

一元运算符中的类型转换

1. 一元加号 +

一元运算符 + 用于将非数字值转换为数字。一元加号运算符用于将其操作数转换为数字。它尝试将非数字值转换为数字。对于字符串,+ 会尝试将其转换为数字;对于数组,空数组转换为 0,非空数组会被转换为其字符串表示的数字。如果操作数已经是数字,则其值保持不变。例如:

// 字符串转换为数字
console.log(+ '123'); // 123 
console.log(+ '123abc'); // NaN
// 布尔值转换
console.log(+ true);  // 1
console.log(+ false); // 0
// 对象和数组: 对象和数组会被转换为其字符串表示形式,然后将其字符串表示转换为数字
console.log(+ []); // 0 (空数组被转换为空字符串 '')
console.log(+ [1, 2]); // NaN (数组被转换为 '1,2')

2. 一元减号 -

一元减号运算符也用于将其操作数转换为数字,并取其负值。转换过程与一元加号类似,但结果是负数。例如:

console.log(- '123'); // -123
console.log(- '123abc'); // NaN
console.log(- true);  // -1
console.log(- false); // 0
console.log(- []); // -0
console.log(- [1, 2]); // NaN

3. 逻辑非运算符 ! 逻辑非运算符用于将其操作数转换为布尔值,并取反。它首先将操作数转换为布尔值,然后返回其逻辑非结果(即 false 转换为 truetrue 转换为 false)。例如:

// **原始值转换**:`0`、`null`、`undefined`、`NaN`、`""`(空字符串)转换为 `false`,其他值(包括对象和数组)转换为 `true`。
console.log(! 0);         // true
console.log(! 'hello');   // false
console.log(! []);        // false

4. 按位取反运算符 ~

按位取反运算符将操作数转换为 32 位有符号整数,并返回其按位取反值。对于非整数值,首先将其转换为整数

// 字符串和数字:字符串会被转换为数字,然后取其按位取反值。
console.log(~ 5); // -6  (5的二进制是 0000 0101,按位取反是 1111 1010,即 -6)
// 对象和数组:对象和数组首先被转换为数字,然后执行按位取反操作。
console.log(~ []); // -1 (空数组被转换为0,按位取反是 -1)

二元运算符 +

二元运算符通常作用于两个操作数。主要的二元运算符包括 +(加法)、-(减法)、*(乘法)、/(除法)、==(相等)、===(严格相等)等。例如:

1. 加法运算符 +

加法运算符用于将两个操作数相加。如果任一操作数是字符串,则将另一个操作数转换为字符串并执行拼接。

console.log(5 + '10'); // "510"
console.log('5' + 10); // "510"
console.log(5 + 10); // 15

2. 减法、乘法和除法运算符 -, *, /

这些运算符在执行操作时,如果操作数中有字符串,JavaScript 会尝试将其转换为数字,然后执行相应的数学运算。

// **字符串转换为数字**:进行标准的数学运算。
console.log('5' - 2); // 3
console.log('5' * 2); // 10
console.log('10' / '2'); // 5
// **非数字值**:如果字符串不能转换为有效数字,结果是 `NaN`。
console.log('5' - 'abc'); // NaN

相等运算符 ==

相等运算符用于比较两个操作数。如果操作数类型不同,JavaScript 会进行类型转换后再进行比较。

  • 转换规则
  • 如果一个操作数是字符串,另一个是数字,则将字符串转换为数字进行比较。
  • nullundefined 被视为相等,但不与其他任何值相等。
  • 布尔值会被转换为数字后进行比较。
console.log(5 == '5'); // true (字符串 '5' 转换为数字 5)
console.log(null == undefined); // true
console.log(0 == false); // true

这里再举个例子,也是曾经出现在面试中的考题,目的就是为了考察你对JS数据类型转换的理解。

题目是:请判断:[] == ![] 的结果是什么?

解题思路是

  1. 等号右边的 ![],这个表达式涉及到布尔值转换。首先,我们需要将 [](空数组)转换为布尔值。 在 JavaScript 中,所有的对象(包括数组)在布尔上下文中都会被转换为 true。所以,[] 转换为布尔值是 true,取反后变为false,于是等号右边的结果是false。这时就变成了[] == false
  2. 根据相等运算符规则,等号两边数据类型不同,就会进行类型转换。根据 JavaScript 的规则,数组会先被转换为字符串,然后再转换为数字。那么首先空数组 [] 会调用数组的 toString 方法(如果得到原始值就直接返回原始值),返回空字符串"",然后空字符串 "" 会被转换为数字 0。这时就变成了0 == false
  3. 最后根据布尔值 false 在数字上下文中也是 0,所以 0 == false就会转变为0 == 0,结果为 true

严格相等运算符 ===

严格相等运算符比较两个操作数时,不进行类型转换,只有类型和值都相等时才返回 true

无类型转换:比较值和类型。

console.log(5 === '5'); // false (不同类型)
console.log(null === undefined); // false (不同类型)
console.log(0 === false); // false (不同类型)

总结

今天我们深入学习了JavaScript中的数据类型转换机制,首先,在 JavaScript 中,数据类型转换主要涉及显式和隐式转换。显式转换包括将原始值或对象显式转换为布尔值、数字、字符串或对象。使用 Boolean(), Number(), String(), 和 new Xxx() 可以直接完成这些转换。隐式转换则发生在比较运算或运算符操作时,例如,当使用 == 运算符比较不同类型的值时,JavaScript 会自动进行类型转换。并且对象转原始值时通常发生隐式转换,具体规则总结如下:

调用 Object.prototype.toString()

  • 所有对象转原始值都会调用 toString()
  1. {}.toSrting() 得到由"[object 和 class 和]" 组成的字符串 [object Object]
  2. [].toString() 返回由数组内部元素以逗号拼接的字符串""
  3. xx.toString() 返回字符串字面量
  • valueOf() 也可以将对象转成原始类型
  1. 包装类对象 比如 Number, String, Boolean 都有 valueOf() 方法

一元运算符(如 +)和二元运算符(如 +-)在操作数的类型转换中也扮演重要角色。一元加号 + 会尝试将其操作数转换为数字,而二元加号 + 在拼接字符串时会将非字符串操作数转换为字符串,在进行数字加法时会将其转换为数字。

最重要的是ToPrimitive 转换规则规定了对象如何转换为原始值。具体步骤总结如下:

  • ToPrimitive(obj, String) ==> String(obj)
  1. 如果接收到的是原始值,直接返回值
  2. 如果接收到的是对象,先调用toString()方法,如果得到原始值,返回值
  3. 如果接收到的是对象,再调用valueOf()方法,如果得到原始值,返回值
  4. 如果以上两种方法都返回原始值,抛出TypeError
  • ToPrimitive(obj, Number) ==> String(obj)
  1. 如果接收到的是原始值,直接返回值
  2. 如果接收到的是对象,先调用valueOf()方法,如果得到原始值,返回值
  3. 如果接收到的是对象,再调用toString()方法,如果得到原始值,返回值
  4. 如果以上两种方法都返回原始值,抛出TypeError