早上好,各位! 今天咱们聊聊 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 对象在 Default hint 下会先调用 toString 。 + 运算符是 Default hint 的典型例子。 |
第二幕: 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 的类型转换。 祝大家编程愉快!