Skip to content

JavaScript 中的数据类型、值与类型转换

很多 JavaScript 问题,看起来像“语法细节”,本质上其实是在问:这个值到底是什么,它是怎么被保存、比较和转换的。

如果这条主线不清楚,后面遇到 =====、浅拷贝、深拷贝、对象比较、隐式转换这些问题时,就会越看越乱。

1. 数据类型到底在说什么

数据类型可以看成 JavaScript 用来描述“一个值是什么、能做什么、会按什么规则参与运算”的分类方式

在 JavaScript 里,常见数据类型通常分成两组:

  1. 原始类型
  2. 引用类型

1.1 原始类型有哪些

原始类型通常包括:

  1. number
  2. string
  3. boolean
  4. undefined
  5. null
  6. symbol
  7. bigint

它们的共同特点可以抓这几条:

  1. 值本身更小、更直接
  2. 通常按值参与复制和比较
  3. 不像对象那样带有一组可随时增删的属性

1.2 引用类型主要是什么

工程里最常见的引用类型,本质上都可以归到 object 这条主线。

例如:

  1. 普通对象
  2. 数组
  3. 函数
  4. 日期对象
  5. 正则对象
  6. MapSet

它们的共同特点是:

  1. 更适合表达结构化数据
  2. 复制时更常见的是“复制引用关系”
  3. 比较时默认比较的不是内容本身,而是是否指向同一个对象

1.3 typeofinstanceofObject.prototype.toString 分别适合干什么

很多人一开始会把“判断类型”理解成只有 typeof 一种写法。

但放到工程里看,这几种方式各有边界:

方式更适合判断什么容易踩的点
typeof原始类型、函数typeof null === 'object'
instanceof某个对象是否来自某个构造器原型链不能直接拿来判断原始类型
Object.prototype.toString.call更细粒度区分内建对象写法相对更长

例如:

js
console.log(typeof 1) // 'number'
console.log(typeof 'a') // 'string'
console.log(typeof null) // 'object'

console.log([] instanceof Array) // true
console.log(new Date() instanceof Date) // true

console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call(new Date())) // [object Date]

真正更稳的经验通常是:

  1. 判断原始类型,优先考虑 typeof
  2. 判断数组、日期等对象类型,不要只靠 typeof
  3. 需要跨场景更稳定地区分对象时,再考虑 Object.prototype.toString.call

2. 为什么 JavaScript 里既有原始类型,又有对象

这是因为它要同时解决两类问题:

  1. 用简单值表达最基础的数据
  2. 用对象表达更复杂的结构和行为

比如:

  1. 年龄、价格、开关状态,更适合用原始类型
  2. 用户信息、商品数据、组件状态,更适合用对象

3. undefinednull 到底怎么区分

这两个词很容易一起出现,但含义并不完全一样。

3.1 undefined

可以看成 有这个变量或属性位子,但当前还没有明确的值

常见场景:

  1. 变量声明了但还没赋值
  2. 访问对象里不存在的属性
  3. 函数没有显式返回值

3.2 null

可以看成 这里明确表示“没有对象”或“当前故意置空”

常见场景:

  1. 初始化时先占位
  2. 某个对象关系被主动清空
  3. 用来表达“此处本应是对象,但当前为空”

3.3 一句话区分

  1. undefined 更像“默认还没有值”
  2. null 更像“明确写出来的空值”

4. 为什么对象比较经常和直觉不一样

看下面这个例子:

js
const a = { name: 'tom' }
const b = { name: 'tom' }

console.log(a === b) // false

很多人第一次看到会觉得奇怪,因为内容明明一样。

真正原因是:对象比较默认比较的是引用是否相同,而不是结构内容是否一样。

也就是说:

  1. ab 虽然内容一样
  2. 但它们不是同一个对象实例
  3. 所以严格比较结果是 false

5. 值复制和引用复制有什么区别

5.1 原始类型复制

js
let a = 10
let b = a
b = 20

console.log(a) // 10
console.log(b) // 20

这里可以看成:

b 拿到的是一份独立的值。

5.2 对象复制

js
const user1 = { name: 'tom' }
const user2 = user1

user2.name = 'jack'

console.log(user1.name) // jack

这里不是“复制了一份完整对象”,而是:

user1user2 指向了同一个对象。

6. 浅拷贝和深拷贝到底在解决什么问题

6.1 浅拷贝

浅拷贝可以看成 只复制当前这一层,里面嵌套的对象仍然共享原来的引用

常见方式:

  1. 展开运算符 ...
  2. Object.assign
  3. 数组的 sliceconcat
js
const a = {
  name: 'tom',
  address: {
    city: 'shanghai'
  }
}

const b = { ...a }
b.address.city = 'beijing'

console.log(a.address.city) // beijing

6.2 深拷贝

深拷贝解决的是:希望得到一份真正独立的嵌套对象结构。

常见方式:

  1. structuredClone
  2. 自己递归实现
  3. 借助工具库

需要注意:

  1. JSON.parse(JSON.stringify(...)) 只能算一种有限场景下的简化方案
  2. 它会丢失 undefined、函数、symbol、日期对象等信息

7. 类型转换为什么容易出坑

JavaScript 的类型转换分两类:

  1. 显式转换
  2. 隐式转换

7.1 显式转换

这是代码明确表达“我要转类型”。

常见方式:

  1. Number(value)
  2. String(value)
  3. Boolean(value)

7.2 隐式转换

这是 JavaScript 在运算或比较时自动帮你转换。

最常见的问题,通常都出在这里。

例如:

js
console.log('5' + 1)   // '51'
console.log('5' - 1)   // 4
console.log(Boolean(''))   // false
console.log(Boolean('0'))  // true

这背后并不是“JavaScript 随机发挥”,而是:不同运算符会触发不同的转换规则。

7.3 对象参与运算时,为什么会先走“转原始值”

很多人能接受字符串和数字互转,但一碰到对象参与 +、比较或模板字符串,就开始觉得结果很玄。

更贴近实际机制的说法是:当对象必须参与原始值运算时,JavaScript 会先尝试把对象转成原始值。

可以把这条过程理解成:

mermaid
flowchart TD
    A[对象参与运算] --> B[尝试转成原始值]
    B --> C[优先找 Symbol.toPrimitive]
    C --> D[没有则继续尝试 valueOf]
    D --> E[仍不行再尝试 toString]
    E --> F[拿到原始值后再继续运算]

例如:

js
const user = {
  valueOf() {
    return 10
  }
}

console.log(user + 1) // 11

这里不是对象自己突然会做加法,而是它先被转成了原始值 10

7.4 为什么 + 和其他运算符的表现差很多

这是隐式转换里最容易让人觉得“JavaScript 在乱来”的地方。

因为 + 不只承担数值加法,还承担字符串拼接。

js
console.log(1 + 2) // 3
console.log('1' + 2) // '12'
console.log('1' + true) // '1true'
console.log('5' - 1) // 4

可以直接把它理解成:

  1. + 只要一边进入字符串语境,就更容易走拼接
  2. -*/ 更偏数值运算,所以会更积极地把两边转成数字

所以很多看似怪异的结果,本质上不是“转换随机”,而是:不同运算符期待的目标类型根本不一样。

7.5 真值和假值为什么也经常让人误判

在条件判断里,JavaScript 并不要求你一定写出 truefalse

很多值都会被自动转成布尔值。

常见假值主要包括:

  1. false
  2. 0
  3. -0
  4. 0n
  5. ''
  6. null
  7. undefined
  8. NaN

例如:

js
if ('0') {
  console.log('run')
}

这段代码会执行,因为 '0' 是非空字符串,属于真值。

工程里一个很常见的问题就是:

  1. 很多人把“看起来像空”误当成假值
  2. '0'[]{} 其实都是真值

所以:条件判断里真正参与运算的,不是你对这个值的直觉,而是 JavaScript 的布尔转换规则。

8. ===== 为什么要区分

8.1 ===

严格相等。

可以看成 不做额外类型转换,类型和值都要匹配

8.2 ==

宽松相等。

可以看成 先尝试按规则做类型转换,再判断是否相等

因为转换链路更复杂,工程上通常更推荐:优先使用 ===,把类型转换写得更明确。

8.3 为什么有些 == 结果看起来非常反直觉

如果只背“尽量别用 ==”,其实还是不够。

更值得理解的是,它到底容易把人带到哪里去。

js
console.log(null == undefined) // true
console.log('' == 0) // true
console.log(false == 0) // true
console.log([] == 0) // true
console.log([] == '') // true

这些结果之所以怪,不是因为规则不存在,而是因为比较前发生了多轮类型转换:

  1. 布尔值比较时,可能先转数字
  2. 对象比较时,可能先转原始值
  3. 字符串和数字比较时,又会继续转数字

也就是说:== 的真正问题不是“偶尔不准”,而是它会把比较过程拉进一条很长的隐式转换链。`

8.4 Object.is 为什么也值得知道

很多人只知道 =====,但工程上还有一个很实用的方法:Object.is

它和 === 大多数时候结果一致,但在两个边界上更精确:

js
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true

console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false

所以可以直接把它理解成:

  1. 日常比较大多还是用 ===
  2. 需要更细分辨 NaN+0-0 这类边界时,可以考虑 Object.is

9. 一张快速对照表

维度原始类型引用类型
典型内容numberstringboolean对象、数组、函数等
复制行为更接近复制值本身更常见是复制引用
比较行为更常见按值比较默认按引用比较
是否适合表达复杂结构不适合适合
常见问题隐式转换、相等判断共享引用、浅拷贝副作用

10. 工程上最容易踩的坑

10.1 误以为对象比较的是内容

对象默认比较的是引用。

10.2 误以为展开运算符就是深拷贝

它只会复制一层。

10.3 把 == 当成普通比较

它背后会触发类型转换,结果可能和直觉不一致。

10.4 误把 nullundefined 完全等同

它们都表示“空”,但语义和使用场景不一样。

11. 小结

这条主线如果压缩成几句话,可以记住:

  1. JavaScript 里的值大体分成原始类型和引用类型
  2. 原始类型更像直接值,对象更像结构化引用
  3. 对象比较默认看引用,不看内容
  4. 很多奇怪结果都和隐式类型转换有关

下一步如果继续往下读,最自然的是进入 函数、作用域、this 与闭包,因为函数执行时“看到了什么”,和这里的值模型是直接连着的。

基于 VitePress 构建的个人技术笔记。