JavaScript 类型转换详解(一)- 基础篇

78 阅读4分钟

💡 更多技术分享,欢迎访问我的博客:叁木の小屋

彻底搞懂 JavaScript 隐式类型转换的核心机制

本文是「JavaScript 类型转换详解」系列的第一篇,介绍类型转换的基础知识和 ToPrimitive 核心机制。建议按顺序阅读,效果更佳。

目录

  1. 引言
  2. 类型转换基础
  3. ToPrimitive 抽象操作
  4. 各种运算符的转换规则

引言

JavaScript 是一门弱类型语言,这意味着在进行运算、比较或拼接时,往往会发生隐式类型转换。理解这些转换规则对于写出正确、可维护的代码至关重要,也是前端面试的高频考点。

为什么 JavaScript 要设计隐式转换?

JavaScript 最初的设计目标是让初学者也能快速上手,因此在很多情况下会自动进行类型转换,减少开发者的心智负担。但这种便利性也带来了很多"陷阱"。


一、类型转换基础

1.1 七种数据类型

首先回顾一下 JavaScript 的七种数据类型:

六种原始类型(Primitive):

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Symbol(ES6)
  • BigInt(ES2020)

一种引用类型:

  • Object(包括 Array、Function、Date、RegExp 等)

1.2 三种类型转换

转换类型触发场景目标类型
ToPrimitive对象转原始值原始类型
ToString字符串上下文String
ToNumber数值上下文Number
ToBoolean逻辑判断Boolean

1.3 转换发生的常见场景

// 1. 算术运算
1 + '1'      // '11'
1 - '1'      // 0
1 * '2'      // 2

// 2. 相等比较
1 == '1'     // true
null == undefined  // true

// 3. 逻辑运算
if ('hello') { }   // 'hello' 被转为 true
'hello' && 123     // 123

// 4. 模板字符串
`1 + 1 = ${1 + 1}`  // '1 + 1 = 2'

// 5. 对象属性访问
obj[1] 和 obj['1']  是一样的

二、ToPrimitive 抽象操作

2.1 ToPrimitive(input, PreferredType?)

当需要将对象转换为原始值时,JavaScript 会调用 ToPrimitive 抽象操作。

// 伪代码表示
function ToPrimitive(input, PreferredType) {
  if (input is Primitive) {
    return input;
  }

  // PreferredType 是 String 或 Number
  // 如果没有指定,默认为 Number(除了 Date 对象)

  let hint = PreferredType || Number;

  // 特殊处理:Date 对象默认 hint 是 String
  if (input is Date) {
    hint = String;
  }

  return OrdinaryToPrimitive(input, hint);
}

2.2 OrdinaryToPrimitive 算法

function OrdinaryToPrimitive(O, hint) {
  if (hint === String) {
    // 优先调用 toString,如果返回原始值则返回
    // 否则调用 valueOf,如果返回原始值则返回
    if (toString 返回原始值) return toString();
    if (valueOf 返回原始值) return valueOf();
  } else if (hint === Number) {
    // 优先调用 valueOf,如果返回原始值则返回
    // 否则调用 toString,如果返回原始值则返回
    if (valueOf 返回原始值) return valueOf();
    if (toString 返回原始值) return toString();
  }
  throw TypeError();
}

2.3 不同 hint 的转换顺序

hint 类型优先调用其次调用
StringtoString()valueOf()
NumbervalueOf()toString()

2.4 内置对象的转换行为

// Number: valueOf 返回数值,toString 返回字符串
let n = 123;
n.valueOf(); // 123
n.toString(); // '123'

// String: valueOf 返回字符串,toString 返回字符串
let s = "hello";
s.valueOf(); // 'hello'
s.toString(); // 'hello'

// Boolean: valueOf 返回布尔值,toString 返回字符串
let b = true;
b.valueOf(); // true
b.toString(); // 'true'

// Array: toString 用逗号连接元素
let arr = [1, 2, 3];
arr.valueOf(); // [1, 2, 3] (不是原始值)
arr.toString(); // '1,2,3' (原始值)

// Object: toString 返回 '[object Object]'
let obj = { a: 1 };
obj.valueOf(); // { a: 1 } (不是原始值)
obj.toString(); // '[object Object]' (原始值)

// Date: 特殊!toString 返回可读日期字符串
let d = new Date();
d.valueOf(); // 1735824000000 (时间戳)
d.toString(); // 'Tue Jan 01 2025 ...' (日期字符串)

// Function: toString 返回函数源代码
let fn = function () {};
fn.valueOf(); // function() {} (不是原始值)
fn.toString(); // 'function() {}' (原始值)

// RegExp: toString 返回正则表达式字符串
let re = /abc/gi;
re.toString(); // '/abc/gi'

2.5 自定义对象的转换

const obj = {
  valueOf() {
    console.log("valueOf called");
    return 42;
  },
  toString() {
    console.log("toString called");
    return "hello";
  },
};

// Number hint: valueOf 优先
+obj // 输出: 'valueOf called', 结果: 42
// String hint: toString 优先
`${obj}`; // 输出: 'toString called', 结果: 'hello'

三、各种运算符的转换规则

3.1 一元运算符 +

// 一元 + 会调用 ToNumber
+"123" + // 123
  "" + // 0
  "true" + // NaN
  true + // 1
  false + // 0
  null + // 0
  undefined + // NaN
  [] + // 0
  ["1"] + // 1
  [1, 2] + // NaN
  {}; // NaN

转换规则:

  1. 调用 ToPrimitive(obj, Number)
  2. 调用 ToNumber(result)

3.2 二元运算符 +

场景 1:至少有一个操作数是 String

1 + '2'         // '12' (1 -> '1')
1 + ''          // '1'
1 + 'true'      // '1true'
null + ''       // 'null' (null -> 'null')
undefined + ''  // 'undefined'
[] + ''         // '' ([] -> '')

场景 2:两个操作数都不是 String

1 + 2; // 3
1 + true; // 2 (true -> 1)
1 + false; // 1 (false -> 0)
1 + null; // 1 (null -> 0)
1 + undefined; // NaN (undefined -> NaN)

true + true; // 2
true + false; // 1
false + false; // 0

null + null; // 0
null + undefined; // NaN

场景 3:对象参与运算

// 关键:先转 primitive,再根据结果判断
[] + []         // ''
[] + {}         // '[object Object]'
{} + []         // 0 (注意:{} 被当作代码块!实际是 +[])
({}) + []       // '[object Object]' (对象转字符串)

[1] + [2]       // '12' (分别转成 '1' 和 '2')
[1, 2] + [3, 4] // '1,23,4'

3.3 算术运算符 -, *, /, %

这些运算符都会将操作数转为 Number:

"5" - 2; // 3 ('5' -> 5)
"5" * 2; // 10
"10" / 2; // 5
"10" % 3; // 1

"5" - true; // 4 (true -> 1)
"5" - null; // 5 (null -> 0)
"5" - []; // 5 ([] -> 0)
"5" - [1]; // 4 ([1] -> 1)
"5" - [1, 2]; // NaN ([1,2] -> NaN)

3.4 关系运算符 >, <, >=, <=

// 字符串比较:按字典序
"10" < "2"; // true (比较字符 '1' < '2')

// 一边是字符串,一边是数字:都转为数字比较
"10" < 2; // false ('10' -> 10)

// 对象转 primitive 再比较
let a = { x: 1 };
a > "[object Object]"; // false (相等)
a >= "[object Object]"; // true

// NaN 的特殊性:任何与 NaN 的比较都是 false
NaN > 0; // false
NaN < 0; // false
NaN >= 0; // false
NaN <= 0; // false

3.5 相等运算符 ==

这是最著名的"陷阱"来源!

3.5.1 基本规则

// 类型相同,直接比较值
1 == 1; // true
"hello" == "hello"; // true

3.5.2 null 和 undefined

null == undefined; // true
null == null; // true
undefined == undefined; // true

// null 和 undefined 不会转换为其他类型
null == 0; // false
null == ""; // false
null == false; // false
undefined == 0; // false
undefined == ""; // false
undefined == false; // false

3.5.3 数字和字符串

// 字符串转数字
"1" == 1; // true
"1" == true; // true (true -> 1, '1' -> 1)
"0" == false; // true (false -> 0, '0' -> 0)
"" == 0; // true ('' -> 0)
"" == false; // true

// 注意:纯数字字符串才会转换
"  1  " == 1; // true (trim 后转数字)
"\n1" == 1; // true
"1a" == 1; // false (转成 NaN)

3.5.4 布尔值和其他类型

// 布尔值先转数字!
true == 1; // true
false == 0; // true
true == "1"; // true (true -> 1, '1' -> 1)
true == "2"; // false (true -> 1, '2' -> 2)
false == ""; // true (false -> 0, '' -> 0)
false == "0"; // true (false -> 0, '0' -> 0)
false == "false"; // false (false -> 0, 'false' -> NaN)

3.5.5 对象和原始值

// 对象转 primitive
[1] == 1          // true ([1] -> '1' -> 1)
[1, 2] == '1,2'   // true
[] == 0           // true ([] -> '' -> 0)
[''] == 0         // true ([''] -> '' -> 0)
['0'] == false    // true (['0'] -> '0' -> 0, false -> 0)

// 对象按地址比较,即使内容相同
let a = [1]
let b = [1]
a == b            // false
a == a            // true

3.5.6 特殊对象

// 包装对象
new Number(1) == 1; // true
new String("1") == "1"; // true
new Boolean(true) == true; // true

// 但注意:
new Number(1) === 1; // false (类型不同!)

小结

本文介绍了 JavaScript 类型转换的基础知识:

  1. 七种数据类型:六种原始类型 + 一种引用类型
  2. ToPrimitive 抽象操作:对象转原始值的核心机制
  3. valueOf 和 toString:不同 hint 下的调用顺序
  4. 各种运算符的转换规则:特别是 + 和 == 的复杂行为

下篇预告: 《JavaScript 类型转换详解(进阶篇)》将深入探讨 9 个经典坑点、Symbol.toPrimitive 的使用,以及显式转换规则对比。


参考资源: