数据类型转换

张玥 2026年9月16日 阅读时间 30分钟
JavaScript 数据类型 类型转换 隐式转换 最佳实践
JavaScript基础之数据类型转换

如果说变量是 JavaScript 的骨架,那么类型就是它的血液。JavaScript 是一门动态弱类型语言,变量不需要声明类型,运行时可以自由地从一个类型"漂移"到另一个类型。这种灵活性带来了便利,也埋下了无数难以排查的 Bug——"1" + 1 === "11"、[] == false、NaN !== NaN……这些"反直觉"的现象背后,其实是一套严谨而完整的抽象类型转换规则。本文将用 30 分钟带你系统梳理 JavaScript 中的显式转换、隐式转换、ToPrimitive 机制、相等性比较与常见陷阱,让你不仅能写对代码,更能讲清楚"为什么"。

数据类型回顾:转换的前提

在讨论转换之前,必须先明确 JavaScript 有哪些数据类型。ES2020 之后,JavaScript 共有 7 种原始类型(Primitive)和 1 种引用类型(Reference)。原始类型按值传递,引用类型按地址传递,这一差异直接影响着转换行为。

/* 7 种原始类型 + 1 种引用类型 */
typeof "hello"      // "string"
typeof 42           // "number"
typeof 9007199254740993n // "bigint"
typeof true         // "boolean"
typeof undefined    // "undefined"
typeof Symbol("id") // "symbol"
typeof null         // "object" ← 历史遗留 bug

/* 引用类型 */
typeof {}            // "object"
typeof []            // "object"
typeof function(){} // "function"
原始类型
string / number
bigint / boolean
undefined / symbol
null
按值存储 · 不可变
引用类型
Object
Array
Function
Date / RegExp
按引用存储 · 可变

类型转换的本质:把值从一种类型映射到另一种类型

类型检测:typeof 的局限与替代方案

转换之前先要知道"当前是什么类型"。typeof 是最常用的操作符,但它对 null 和数组的判断并不准确,需要配合其他手段。

/* typeof 的三大"坑" */
typeof null         // "object" ✗
typeof []           // "object" ✗
typeof NaN          // "number" △

/* 更精确的检测方式 */
Array.isArray([])           // true
Number.isNaN(NaN)         // true(不会隐式转换)
Object.is(NaN, NaN)        // true
[] instanceof Array          // true

/* 万能方案:Object.prototype.toString */
const type = (v) =>
  Object.prototype.toString.call(v).slice(8, -1);

type(null)       // "Null"
type([])         // "Array"
type(new Date()) // "Date"
type(/a/g)        // "RegExp"
推荐 Array.isArray() 数组
推荐 Number.isNaN() NaN
万能 toString.call() 任意类型
原型 instanceof 自定义类

先准确判断类型,再决定如何转换

显式转换之 Number()

Number() 是最严格的数字转换函数,它会把整个值作为一个整体进行解析,任何"多余"的字符都会导致结果为 NaN。

/* 字符串 → 数字 */
Number("123")     // 123
Number(" 42 ")    // 42(自动去首尾空格)
Number("")        // 0 ← 注意!
Number(" ")      // 0 ← 纯空格也是 0
Number("12abc")  // NaN
Number("0x1F")   // 31(支持十六进制)
Number("1e3")    // 1000(支持科学计数法)

/* 布尔值 / null / undefined */
Number(true)      // 1
Number(false)     // 0
Number(null)      // 0
Number(undefined) // NaN

/* 对象 → 数字(走 ToPrimitive) */
Number([])       // 0
Number([5])      // 5
Number([1, 2])   // NaN
Number({})       // NaN
Number() 转换规则
undefined NaN
null 0
true / false 1 / 0
"" / " " 0
"12abc" NaN

空字符串转成 0,是很多"幽灵 Bug"的源头

显式转换之 String()

String() 是显式字符串转换,它几乎不会失败——任何值都能变成字符串,这也是它和 Number() 最大的区别。

/* 基本类型 → 字符串 */
String(123)        // "123"
String(NaN)        // "NaN"
String(Infinity)   // "Infinity"
String(true)       // "true"
String(null)       // "null"
String(undefined)  // "undefined"

/* 对象 → 字符串 */
String([])           // ""
String([1, 2, 3]) // "1,2,3"
String({})           // "[object Object]"
String(new Date())  // "Wed Sep 16 2026 ..."

/* Symbol 是特例 */
String(Symbol("a")) // "Symbol(a)" ✓
Symbol("a") + ""      // ✗ TypeError
✓ 安全的写法
String(value)
对 Symbol 也能正常工作
△ 常见写法
value + ""
简洁,但对 Symbol 会抛错
✗ 危险的写法
value.toString()
null / undefined 会直接报错

不确定值时,优先使用 String()

显式转换之 Boolean() 与假值表

布尔转换最简单,也最容易被忽视。记住 8 个假值(falsy),其余一切都是真值(truthy),这是 JavaScript 中性价比最高的记忆点之一。

/* 完整的假值列表(只有这 8 个) */
Boolean(false)      // false
Boolean(0)          // false
Boolean(-0)         // false
Boolean(0n)         // false
Boolean("")         // false
Boolean(null)       // false
Boolean(undefined)  // false
Boolean(NaN)        // false

/* 容易误判的"真值" */
Boolean("0")       // true ← 非空字符串
Boolean(" ")       // true ← 空格也是字符
Boolean([])        // true ← 空数组也是对象
Boolean({})        // true
Boolean(function(){} ) // true
全部假值(8 个)
false 0 -0 0n "" null undefined NaN
if (arr.length) → 判断数组非空 ✓
if (str) → 判断字符串非空 ✓
if (obj) → 永远为真,需谨慎 ✗

parseInt / parseFloat:解析而非转换

parseInt 和 parseFloat 与 Number() 有本质区别:它们是从左到右逐个字符解析,遇到无法解析的字符就停止,返回已解析的部分。

/* parseInt:解析整数 */
parseInt("42px")    // 42 ← 遇到 p 停止
parseInt("3.99")    // 3 ← 截断小数
parseInt("abc")     // NaN
parseInt(" 10 ") // 10 ← 忽略首尾空格

/* 第二个参数:进制(强烈建议显式指定) */
parseInt("11", 2)   // 3 二进制
parseInt("ff", 16)  // 255 十六进制
parseInt("08")      // 8(ES5+ 默认十进制)

/* parseFloat:解析浮点数 */
parseFloat("3.14abc") // 3.14
parseFloat(".5")      // 0.5
parseFloat("1e3")    // 1000
parseFloat("0x10")  // 0 ← 不支持十六进制

/* 对比:Number() 更严格 */
Number("42px")      // NaN
parseInt("42px")    // 42
三者的选择策略
Number(v)
严格转换 · 有非法字符即 NaN · 适合表单纯数字校验
parseInt(v, 10)
解析整数 · 容忍后缀 · 适合读取 CSS 像素值
parseFloat(v)
解析浮点 · 容忍后缀 · 适合带单位的小数

parseInt 第二个参数永远写 10,避免历史遗留问题

隐式转换(一):+ 运算符的"双重人格"

加号是 JavaScript 中最"分裂"的运算符:它既能做数学加法,也能做字符串拼接。到底走哪条路,取决于操作数的类型。

/* 规则:只要有一个操作数是字符串,就做拼接 */
1 + 2        // 3
"1" + 2      // "12"
1 + "2"      // "12"
1 + 2 + "3"  // "33" ← 从左到右
"1" + 2 + 3  // "123"

/* 其他类型参与 + 运算 */
true + 1       // 2 (true → 1)
null + 1       // 1 (null → 0)
undefined + 1 // NaN
[] + []         // ""
[] + {}         // "[object Object]"
{} + []         // 0 ← 语句块 + 一元加号

/* 其他算术运算符一律转为数字 */
"5" - 2       // 3
"5" * "2"    // 10
"6" / "2"    // 3
"abc" - 1   // NaN
+ 运算符决策流程
对两个操作数执行 ToPrimitive
↓
有任意一个是 string ?
是 ↓
字符串拼接
否 ↓
转为 Number 相加

只有 + 会做字符串拼接,其余算术运算符都转为数字

隐式转换(二):== 与 === 的世纪之争

== 会在比较前进行类型转换,=== 则严格比较类型和值。理解 == 的转换规则,能帮你避开 90% 的相等性陷阱。

/* === 严格相等:类型不同直接 false */
1 === "1"       // false
null === undefined // false
NaN === NaN     // false ← 唯一不等于自身的值

/* == 宽松相等:会做类型转换 */
1 == "1"        // true → "1" 转 1
0 == ""         // true → "" 转 0
0 == false      // true → false 转 0
null == undefined // true ← 特例,互相相等
null == 0        // false ← 特例
NaN == NaN      // false

/* 经典"迷惑行为" */
[] == false       // true
[] == ![]         // true 😱
"" == false      // true
[1] == 1        // true
== 转换规则速查
① 同类型 → 直接比较
② null == undefined → true
③ number == string → string 转 number
④ boolean → 先转 number 再比较
⑤ object == primitive → object 走 ToPrimitive
实践建议
默认使用 ===;仅当明确需要 == null 来同时判断 null 和 undefined 时使用 ==

对象转原始值:ToPrimitive 机制

当对象参与运算时,引擎会调用内部的 ToPrimitive 抽象操作。理解 Symbol.toPrimitive、valueOf、toString 的调用顺序,是进阶的关键。

/* ToPrimitive 的调用优先级 */
// 1. Symbol.toPrimitive(hint) ← 最高
// 2. valueOf() → hint 为 "number"/"default"
// 3. toString() → hint 为 "string"

const obj = {
  valueOf() { return 42; },
  toString() { return "obj"; }
};

obj + 1        // 43(default → valueOf)
String(obj)   // "obj"(string → toString)
Number(obj)   // 42(number → valueOf)

/* 自定义 Symbol.toPrimitive */
const price = {
  amount: 100,
  [Symbol.toPrimitive](hint) {
    if (hint === "string") return `¥${this.amount}`;
    return this.amount;
  }
};

`价格:${price}`  // "价格:¥100"
price * 2          // 200
hint 的三种取值
"number" Number()、*、-
"string" String()、模板字符串
"default" +、==、比较运算

大多数内置对象默认走 valueOf,Date 是唯一默认走 toString 的

JSON 序列化与反序列化

与前端最密切相关的类型转换场景之一,就是 JSON 的字符串化与解析。它能"丢失"一部分类型信息,这是很多数据 Bug 的根源。

/* 序列化:对象 → JSON 字符串 */
JSON.stringify({ a: 1, b: "x" });
// '{"a":1,"b":"x"}'

/* 解析:JSON 字符串 → 对象 */
JSON.parse('{"a":1}') // { a: 1 }
JSON.parse('{"a":1}').a   // 1(number)

/* 会"消失"或被改变的类型 */
JSON.stringify({
  u: undefined,     // 被省略
  f: function(){} // 被省略
  s: Symbol()   // 被省略
  n: NaN         // → null
  d: new Date()  // → ISO 字符串
});

/* 大整数精度丢失问题 */
JSON.parse('{"id":9007199254740993}').id;
// 9007199254740992 ← 精度丢失!
JSON 无法保真的类型
undefined · Function · Symbol
NaN / Infinity → null
Date → 字符串
RegExp → {}
安全的解析方式
try {
  const data = JSON.parse(str);
} catch (e) {
  console.error("非法 JSON");
}

常见陷阱大盘点

下面这些场景在实际项目中反复出现,几乎每一个都曾让开发者"怀疑人生"。

/* 陷阱 1:表单输入都是字符串 */
const a = input1.value; // "5"
const b = input2.value; // "3"
a + b           // "53" ✗ 想要 8
Number(a) + Number(b) // 8 ✓

/* 陷阱 2:小数精度 */
0.1 + 0.2       // 0.30000000000000004
(0.1 + 0.2).toFixed(2) // "0.30"

/* 陷阱 3:假值判断 */
const count = 0;
if (count) { ... }  // ✗ 0 是假值,不会执行
if (count !== undefined) { ... } // ✓

/* 陷阱 4:sort 默认按字符串排序 */
[10, 9, 1].sort();
// [1, 10, 9] ✗
[10, 9, 1].sort((a, b) => a - b);
// [1, 9, 10] ✓

/* 陷阱 5:空数组/空对象的布尔值 */
if ([]) console.log("执行"); // 会执行!
if (list.length) { ... } // 正确写法
✗ 错误
"5" + "3" → "53"
✓ 正确
Number("5") + Number("3") → 8
✗ 错误
if (count) → 0 被忽略
✓ 正确
if (count !== undefined)

知道陷阱在哪,才能提前绕开

类型转换最佳实践总结

  • 显式优于隐式:能用 Number()、String()、Boolean() 就不要依赖隐式转换
  • 默认使用 ===:只有 value == null 这种同时判断 null 和 undefined 的场景才用 ==
  • parseInt 显式指定进制:永远写 parseInt(str, 10)
  • 数字校验用 Number.isNaN:而不是全局的 isNaN,后者会先做类型转换
  • 字符串转换优先 String():比 .toString() 更安全,比 + "" 更严谨
  • 判断数组是否为空:用 arr.length,而不是直接判断 arr
  • 大整数用 BigInt 或字符串:避免超过 Number.MAX_SAFE_INTEGER 后精度丢失
  • 金额计算避免浮点:转成整数(分)计算,或使用专门的十进制库
  • JSON 解析要 try/catch:外部数据永远不可信
  • 用 TypeScript 兜底:编译期类型检查能从源头消灭大量转换问题
/* 一套安全转换工具函数 */
const toNumber = (v, fallback = 0) => {
  const n = Number(v);
  return Number.isNaN(n) ? fallback : n;
};

const toString = (v) => (v == null ? "" : String(v));

const toBoolean = (v) => Boolean(v);

/* 使用 */
toNumber("42px", 0)  // 0(严格模式)
toNumber("42", 0)    // 42
toString(null)          // ""