早上好,各位! 今天咱们聊聊 JavaScript 里那些“暗箱操作”——类型转换(Coercion)。 别害怕,这玩意听起来玄乎,其实就像魔术,你知道原理了,也就那么回事。
开场白:JavaScript 的 “七十二变”
JavaScript 就像个性格多变的演员,同一个值,在不同的场合,可以扮演不同的角色。 比如,数字 5,既可以老老实实当个数字,也可以摇身一变成字符串 "5"。 这就是类型转换在搞鬼。 类型转换分两种:
- 显式类型转换 (Explicit Coercion):  你主动要求它变。 比如 String(5),Number("42")。
- 隐式类型转换 (Implicit Coercion):  JavaScript 偷偷摸摸地帮你变。 比如 5 + "5",if (0)。
今天咱们重点聊聊这“偷偷摸摸”的隐式类型转换,因为这才是 Bug 的温床,也是面试官最爱挖坑的地方。
第一幕: ToPrimitive – 类型转换的幕后推手
所有类型转换,最终都要落到原始类型(primitive types)上。JavaScript 有七种原始类型:
- String
- Number
- Boolean
- Null
- Undefined
- Symbol(ES6 新增)
- BigInt(ES2020 新增)
所以,当 JavaScript 遇到一个非原始类型(比如对象、数组),需要把它转换成原始类型时,就会调用一个内部操作:ToPrimitive。
ToPrimitive 的算法大致如下:
- 检查值是否已经是原始类型: 如果是,直接返回。
- 否则,调用 valueOf()方法: 如果valueOf()返回原始类型,返回结果。
- 否则,调用 toString()方法: 如果toString()返回原始类型,返回结果。
- 如果以上两个方法都没返回原始类型,报错。 (TypeError)
但是,这个过程会受到一个 "hint" 的影响,这个 "hint" 告诉 ToPrimitive 期望转换成什么类型的原始值:
- Number: 期望转换成数字类型。
- String: 期望转换成字符串类型。
- Default:  没指定,通常发生在 ==比较和+运算符中。
这个 "hint" 会影响 valueOf() 和 toString() 的调用顺序。
举个栗子:
let obj = {
  valueOf: function() {
    console.log("valueOf called");
    return {}; // 返回非原始值
  },
  toString: function() {
    console.log("toString called");
    return "hello"; // 返回字符串
  }
};
console.log(String(obj)); // hint 是 String, 先 toString
// 输出:
// toString called
// hello
console.log(Number(obj)); // hint 是 Number, 先 valueOf
// 输出:
// valueOf called
// toString called
// NaN
console.log(obj + ""); // hint 是 Default, 先 valueOf (Date 对象除外)
// 输出:
// valueOf called
// toString called
// [object Object]总结一下:
| Hint | valueOf是否优先调用 | 备注 | 
|---|---|---|
| Number | 是 | 除非对象重写了 valueOf方法,否则会继续调用toString。 如果valueOf和toString都没返回原始值,则抛出 TypeError。 | 
| String | 否 | 先尝试 toString,如果返回原始值,就用这个值。 否则,调用valueOf,如果返回原始值,就用这个值。 如果valueOf和toString都没返回原始值,则抛出 TypeError。 | 
| Default | 大部分情况是 | 除了 Date对象,其他对象都先调用valueOf。Date对象在Defaulthint 下会先调用toString。+运算符是Defaulthint 的典型例子。 | 
第二幕: ToString – 如何变成字符串?
ToString  负责把各种值转换成字符串。 规则如下:
| 类型 | 转换结果 | 
|---|---|
| Undefined | "undefined" | 
| Null | "null" | 
| Boolean | true->"true",false->"false" | 
| Number | 按照数字字面量规则 | 
| Symbol | 抛出 TypeError | 
| BigInt | 按照数字字面量规则 | 
| Object | 调用 ToPrimitive,hint 是 String | 
举个栗子:
console.log(String(undefined)); // "undefined"
console.log(String(null)); // "null"
console.log(String(true)); // "true"
console.log(String(42)); // "42"
let sym = Symbol("foo");
//console.log(String(sym)); // TypeError: Cannot convert a Symbol value to a string
let obj = { name: "Alice" };
console.log(String(obj)); // "[object Object]"  (因为默认的 toString 方法返回这个)第三幕: ToNumber – 如何变成数字?
ToNumber 负责把各种值转换成数字。 规则如下:
| 类型 | 转换结果 | 
|---|---|
| Undefined | NaN | 
| Null | 0 | 
| Boolean | true->1,false->0 | 
| Number | 不变 | 
| String | 如果是数字字面量,转换为数字;否则 NaN | 
| Symbol | 抛出 TypeError | 
| BigInt | 如果是数字字面量,转换为数字;否则抛出 TypeError | 
| Object | 调用 ToPrimitive,hint 是 Number | 
举个栗子:
console.log(Number(undefined)); // NaN
console.log(Number(null)); // 0
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number("42")); // 42
console.log(Number("hello")); // NaN
let sym = Symbol("foo");
//console.log(Number(sym)); // TypeError: Cannot convert a Symbol value to a number
let obj = { valueOf: () => 5 };
console.log(Number(obj)); // 5第四幕: ToBoolean – 如何变成布尔值?
ToBoolean 负责把各种值转换成布尔值。 规则很简单:
- Falsy 值:  false,0,"",NaN,null,undefined,-0,+0。 这些值会被转换为false。
- Truthy 值:  除了 Falsy 值以外的所有值,都会被转换为 true。
举个栗子:
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean(1)); // true
console.log(Boolean("hello")); // true
console.log(Boolean({})); // true
console.log(Boolean([])); // true  (空数组也是 Truthy 值!)重点来了:隐式类型转换的重灾区
理解了 ToPrimitive、ToString、ToNumber、ToBoolean,我们就来看看隐式类型转换的重灾区:
- 
+运算符:- 如果 +运算符的操作数中有一个是字符串,那么执行字符串拼接。
- 否则,都转换为数字进行加法运算。
 console.log(1 + "1"); // "11" (字符串拼接) console.log(1 + 1); // 2 (数字加法) console.log(1 + true); // 2 (true 转换为 1) console.log(1 + null); // 1 (null 转换为 0) console.log(1 + undefined); // NaN (undefined 转换为 NaN) console.log([] + []); // "" (空数组转换为空字符串) console.log([] + {}); // "[object Object]" console.log({} + []); // 浏览器环境输出"[object Object]0",Node.js环境抛出错误 console.log({} + {}); // "[object Object][object Object]" (在某些浏览器中,可能解释为代码块,导致结果不同)注意 {} + []这种情况。 在浏览器中,{}会被解释为一个空的代码块,所以实际上执行的是+ [],结果是0,然后和"[object Object]"相加。 在 Node.js 中,{}被当做一个空对象,会抛出错误。
- 如果 
- 
==运算符:==运算符的类型转换规则非常复杂,是面试官最喜欢考察的知识点。 简单来说,它会尝试将两边的操作数转换为相同的类型,然后再进行比较。- 如果类型相同,直接比较。
- 如果类型不同:
- null == undefined// true
- 如果一个是数字,一个是字符串,将字符串转换为数字再比较。
- 如果一个是布尔值,将其转换为数字再比较 (true -> 1, false -> 0)。
- 如果一个是对象,另一个是数字或字符串,则使用 ToPrimitive将对象转换为原始值再比较。
 
 console.log(null == undefined); // true console.log(0 == ""); // true (字符串 "" 转换为 0) console.log(1 == true); // true (true 转换为 1) console.log("1" == true); // true (true 转换为 1, "1" 转换为 1) console.log([] == false); // true ([] 转换为 "", "" 转换为 0, false 转换为 0) console.log([] == ![]); // true (![] 是 false,[] == false) console.log(2 == [2]); // true, [2] -> "2" -> 2 console.log("0" == false); // true, false -> 0 console.log(0 == false); // true console.log(false == "false"); // false, "false" -> NaN console.log(null == false); // false, null 只能和 undefined 相等 console.log(undefined == false); // false, undefined 只能和 null 相等永远记住: 尽量避免使用 ==,使用===(严格相等) 可以避免很多不必要的类型转换。===不会进行类型转换,只有类型和值都相等时,才会返回true。
- 
关系运算符 ( >,<,>=,<=):- 如果操作数都是字符串,按照 Unicode 码点进行比较。
- 否则,都转换为数字进行比较。
 console.log("2" > 1); // true ("2" 转换为 2) console.log("abc" > "abd"); // false (按照 Unicode 码点比较) console.log("abc" > 1); // false ("abc" 转换为 NaN, NaN 和任何数字比较都返回 false) console.log(NaN > 1); // false console.log(NaN < 1); // false console.log(NaN == 1); // false console.log(NaN != 1); // true
- 
逻辑运算符 ( &&,||,!):- &&和- ||不会进行类型转换,它们返回的是操作数本身的值。 但是,它们会利用- ToBoolean判断操作数的 Truthy/Falsy。
- !会将操作数转换为布尔值,然后取反。
 console.log(1 && 2); // 2 (1 是 Truthy, 返回第二个操作数) console.log(0 && 2); // 0 (0 是 Falsy, 返回第一个操作数) console.log(1 || 2); // 1 (1 是 Truthy, 返回第一个操作数) console.log(0 || 2); // 2 (0 是 Falsy, 返回第二个操作数) console.log(!0); // true (0 是 Falsy, 取反后是 true) console.log(!""); // true ("" 是 Falsy, 取反后是 true)
第五幕:避免掉坑指南
- 
使用 ===代替==: 这是最有效的避免类型转换陷阱的方法。
- 
明确类型: 在进行运算之前,明确变量的类型,可以使用 typeof运算符进行检查。
- 
显式转换: 如果需要类型转换,尽量使用显式转换,例如 Number(),String(),Boolean()。
- 
注意 NaN:NaN和任何值比较(包括自身)都返回false。 使用isNaN()函数判断一个值是否是NaN。 (注意:isNaN()也会进行类型转换,建议使用Number.isNaN(),它不会进行类型转换。)
- 
小心处理对象: 当对象参与运算时,要清楚 valueOf()和toString()方法的行为。
总结陈词: 掌握类型转换,驾驭 JavaScript
类型转换是 JavaScript 的一个重要特性,也是一个容易出错的地方。 理解了类型转换的原理,就可以避免很多 Bug,写出更健壮的代码。 记住,清晰的代码胜过任何花哨的技巧。 尽量让你的代码表达出明确的意图,减少隐式类型转换带来的歧义。
希望今天的讲座能帮助大家更好地理解 JavaScript 的类型转换。 祝大家编程愉快!