💡 更多技术分享,欢迎访问我的博客:叁木の小屋
9 个经典坑点与避坑指南
本文是「JavaScript 类型转换详解」系列的第二篇,深入探讨常见的类型转换陷阱和高级特性。建议先阅读[基础篇](JavaScript 类型转换详解(一)- 基础篇本文是「JavaScript 类型转换详解」系列的第一篇,介绍类型转换 - 掘金)。
目录
一、常见坑点详解
1.1 坑 1:+ 运算符的二义性
// 看起来相加,实际是拼接
"1" + 1; // '11'
1 + "1"; // '11'
1 + 1 + "1"; // '21' (先算 1+1=2,再拼接 '1')
"1" + 1 + 1; // '111' (从左到右拼接)
// 实际开发中的坑
function add(a, b) {
return a + b; // 期望加法,可能变成拼接
}
add("5", 5); // '55' 而不是 10!
解决方案:
// 显式转换
function add(a, b) {
return Number(a) + Number(b);
}
1.2 坑 2:== 的意外行为
// 这些结果可能让你惊讶
[] == false; // true
![] == false; // true
![] == []; // true (![] -> false, [] -> false -> 0)
// 解释:
// [] == false
// 1. [] -> '' (ToPrimitive)
// 2. '' -> 0 (ToNumber)
// 3. false -> 0 (ToNumber)
// 4. 0 == 0 -> true
记住: 优先使用 === 而不是 ==
1.3 坑 3:假值不等于假值
// 这些都是假值,但它们不相等!
false == 0; // true
false == ""; // true
0 == ""; // true
null == 0; // false!
undefined == 0; // false!
null == undefined; // true
null == false; // false
null == ""; // false
undefined == false; // false
undefined == ""; // false
// 记住:只有 null == undefined 是例外
1.4 坑 4:对象转字符串
// 空数组的转换
[] + 1 // '1' ([] -> '')
[] - 1 // -1 ([] -> '' -> 0)
// 非空数组
[1] + 1 // '11' ([1] -> '1')
[1] - 1 // 0 ([1] -> '1' -> 1)
[1, 2] + 1 // '1,21' ([1,2] -> '1,2')
[1, 2] - 1 // NaN ([1,2] -> '1,2' -> NaN)
// 对象
{} + 1 // 1 ({} 被当作代码块!)
({}) + 1 // '[object Object]1'
[] + {} // '[object Object]'
注意: {} 在行首会被解析为代码块,用 ({}) 强制转为表达式
1.5 坑 5:ToBoolean 的规则
// 假值(falsy)列表 - 只有这 8 个!
Boolean(false); // false
Boolean(0); // false
Boolean(-0); // false
Boolean(0n); // false
Boolean(""); // false
Boolean(null); // false
Boolean(undefined); // false
Boolean(NaN); // false
// 其他所有值都是真值(truthy)
Boolean("0"); // true
Boolean("false"); // true
Boolean([]); // true - 空数组是真值!
Boolean({}); // true - 空对象是真值!
Boolean(function () {}); // true
1.6 坑 6:Number() 的怪异行为
Number(""); // 0
Number(" "); // 0
Number("\n"); // 0
Number(" 123"); // 123
Number("123 "); // 123
Number("123a"); // NaN
Number("a123"); // NaN
Number("0x10"); // 16 (支持十六进制)
Number("0b10"); // 2 (支持二进制)
Number("0o10"); // 8 (支持八进制)
Number(null); // 0
Number(undefined); // NaN
Number(true); // 1
Number(false); // 0
Number() vs parseInt() 的区别:
// Number(): 严格转换,整个字符串必须是有效数字
Number("123px"); // NaN
// parseInt(): 解析到非数字字符停止
parseInt("123px"); // 123
1.7 坑 7:toString 和 valueOf 的返回值
// 如果 toString/valueOf 返回的不是原始值
const obj1 = {
toString() { return {}; } // 返回对象
};
String(obj1) // TypeError!
const obj2 = {
valueOf() { return {}; } // 返回对象
toString() { return 'hello'; }
};
Number(obj2) // 'hello' -> NaN (valueOf 失败,fallback 到 toString)
关键: toString/valueOf 必须返回原始值,否则会继续尝试下一个方法
1.8 坑 8:Date 的特殊行为
// Date 在 ToPrimitive 时 hint 默认是 String
const d = new Date("2025-01-01");
String(d); // 'Wed Jan 01 2025 00:00:00 GMT+0800'
Number(d); // 1735660800000
// 这导致一些有趣的结果
const d1 = new Date("2025-01-01");
const d2 = new Date("2025-01-01");
d1 == d2; // false (对象比较地址)
d1.getTime() == d2.getTime(); // true
+d1 == +d2; // true
记住: Date 是唯一默认 hint 为 String 的对象
1.9 坑 9:BigInt 的类型转换
// BigInt 与 Number 混合运算会报错
1n + 1; // TypeError!
1n == 1; // false (类型不同)
1n === 1; // false
// 比较
1n > 1; // false
1n >= 1; // true
// 布尔转换
Boolean(0n); // false
Boolean(1n); // true
if (0n) {
} // 不执行
// 与 String
"1" == 1n; // true ('1' -> 1, 然后 1 == 1n)
二、Symbol.toPrimitive
ES6 引入了 Symbol.toPrimitive 来精确控制对象的类型转换行为,优先级高于 valueOf 和 toString。
2.1 基本用法
const obj = {
[Symbol.toPrimitive](hint) {
console.log("hint:", hint);
switch (hint) {
case "number":
return 42;
case "string":
return "hello";
case "default":
return "default";
}
},
};
+obj // hint: number, 42
`${obj}`; // hint: string, 'hello'
obj + obj; // hint: default, 'default'
2.2 hint 的三种值
| hint 值 | 触发场景 |
|---|---|
| 'number' | +obj, obj - 2, obj* 3, obj / 4, obj > 5, obj == 6 (如果另一边是 number) |
| 'string' | ${obj}, String(obj), obj + '' (如果另一边是 string) |
| 'default' | obj + obj, obj == obj, obj == 'string' |
2.3 优先级演示
const obj = {
[Symbol.toPrimitive](hint) {
console.log("toPrimitive:", hint);
return "from toPrimitive";
},
valueOf() {
console.log("valueOf");
return "from valueOf";
},
toString() {
console.log("toString");
return "from toString";
},
};
+obj;
// 输出: toPrimitive: number
// 结果: NaN ('from toPrimitive' -> NaN)
String(obj);
// 输出: toPrimitive: string
// 结果: 'from toPrimitive'
obj + "";
// 输出: toPrimitive: default
// 结果: 'from toPrimitive'
2.4 实际应用:金额对象
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return this.amount;
}
if (hint === 'string') {
return `${this.amount} ${this.currency}`;
}
return this.amount;
}
}
const price = new Money(99.99, 'CNY');
+price // 99.99
String(price) // '99.99 CNY'
price + 10 // 109.99
三、显式转换规则对比
3.1 Number() vs parseInt()
// Number(): 严格转换
Number("123"); // 123
Number("123px"); // NaN
Number(""); // 0
Number(" 123"); // 123
Number("0x10"); // 16
// parseInt(): 解析到非数字字符停止
parseInt("123"); // 123
parseInt("123px"); // 123
parseInt(""); // NaN
parseInt(" 123"); // 123
parseInt("0x10"); // 16
// parseFloat(): 类似 parseInt,但支持小数
parseFloat("3.14"); // 3.14
parseFloat("3.14px"); // 3.14
parseFloat("0.1.2"); // 0.1
何时使用哪个?
- 需要严格转换整个字符串 → 用
Number() - 需要从字符串开头提取数字 → 用
parseInt()
3.2 String() vs toString()
// String(): 处理 null 和 undefined
String(null); // 'null'
String(undefined); // 'undefined'
// toString(): null 和 undefined 会报错
null.toString(); // TypeError!
undefined.toString(); // TypeError!
// 对对象的处理相同
String([1, 2]) // '1,2'
[(1, 2)].toString(); // '1,2'
String({}); // '[object Object]'
({}).toString(); // '[object Object]'
推荐: 统一使用 String(),更安全
3.3 Boolean() 转换
// 使用 Boolean() 或 !!(双重否定)
Boolean(0); // false
!!0; // false
Boolean(""); // false
!!""; // false
Boolean("0"); // true
!!"0"; // true
Boolean([]); // true
!![]; // true
Boolean({}); // true
!!{}; // true
四、JSON.stringify 的转换
JSON.stringify 在转换对象时也会调用 toJSON 或 toString。
4.1 基本转换
// 原始值
JSON.stringify(123); // '123'
JSON.stringify("hello"); // '"hello"'
JSON.stringify(true); // 'true'
JSON.stringify(null); // 'null'
// 数组
JSON.stringify([1, 2, 3]); // '[1,2,3]'
JSON.stringify([1, , 3]); // '[1,null,3]' (undefined 转为 null)
// 对象
JSON.stringify({ a: 1 }); // '{"a":1}'
JSON.stringify({ a: undefined }); // '{}' (undefined 会被忽略)
4.2 toJSON 方法
const obj = {
name: "John",
toJSON() {
return "custom";
},
};
JSON.stringify(obj); // '"custom"'
4.3 特殊值处理
// undefined、function、symbol:在对象中忽略,在数组中转为 null
JSON.stringify({ a: undefined }); // '{}'
JSON.stringify({ a: function () {} }); // '{}'
JSON.stringify({ a: Symbol() }); // '{}'
JSON.stringify([undefined, function () {}, Symbol()]); // '[null,null,null]'
// NaN、Infinity、null:转为 null
JSON.stringify(NaN); // 'null'
JSON.stringify(Infinity); // 'null'
JSON.stringify(null); // 'null'
// 循环引用:报错
const a = {};
a.b = a;
JSON.stringify(a); // TypeError: Converting circular structure to JSON
4.4 实用技巧:格式化输出
const obj = { name: "John", age: 30 };
// 格式化输出(2 空格缩进)
JSON.stringify(obj, null, 2);
/*
{
"name": "John",
"age": 30
}
*/
// 过滤敏感字段
const user = {
name: "John",
password: "secret123",
email: "john@example.com"
};
JSON.stringify(user, ['name', 'email']);
// '{"name":"John","email":"john@example.com"}'
小结
本篇深入探讨了 JavaScript 类型转换的进阶内容:
- 9 个经典坑点:从 + 运算符到 BigInt 的特殊行为
- Symbol.toPrimitive:ES6 精确控制类型转换的利器
- 显式转换对比:Number vs parseInt、String vs toString
- JSON.stringify:序列化时的特殊转换规则
核心建议:
- 优先使用
===而不是== - 使用显式转换避免意外
- 了解
Symbol.toPrimitive处理复杂场景
下篇预告: 《JavaScript 类型转换详解(实战篇)》将带来面试真题实战和最佳实践指南。
参考资源: