JavaScript 类型转换详解(二)- 进阶篇

93 阅读6分钟

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

9 个经典坑点与避坑指南

本文是「JavaScript 类型转换详解」系列的第二篇,深入探讨常见的类型转换陷阱和高级特性。建议先阅读[基础篇](JavaScript 类型转换详解(一)- 基础篇本文是「JavaScript 类型转换详解」系列的第一篇,介绍类型转换 - 掘金)。

目录

  1. 常见坑点详解
  2. Symbol.toPrimitive
  3. 显式转换规则对比
  4. JSON.stringify 的转换

一、常见坑点详解

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 来精确控制对象的类型转换行为,优先级高于 valueOftoString

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 在转换对象时也会调用 toJSONtoString

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 类型转换的进阶内容:

  1. 9 个经典坑点:从 + 运算符到 BigInt 的特殊行为
  2. Symbol.toPrimitive:ES6 精确控制类型转换的利器
  3. 显式转换对比:Number vs parseInt、String vs toString
  4. JSON.stringify:序列化时的特殊转换规则

核心建议:

  • 优先使用 === 而不是 ==
  • 使用显式转换避免意外
  • 了解 Symbol.toPrimitive 处理复杂场景

下篇预告: 《JavaScript 类型转换详解(实战篇)》将带来面试真题实战和最佳实践指南。


参考资源: