Appearance
JavaScript 中的数据类型、值与类型转换
很多 JavaScript 问题,看起来像“语法细节”,本质上其实是在问:这个值到底是什么,它是怎么被保存、比较和转换的。
如果这条主线不清楚,后面遇到 ==、===、浅拷贝、深拷贝、对象比较、隐式转换这些问题时,就会越看越乱。
1. 数据类型到底在说什么
数据类型可以看成 JavaScript 用来描述“一个值是什么、能做什么、会按什么规则参与运算”的分类方式。
在 JavaScript 里,常见数据类型通常分成两组:
- 原始类型
- 引用类型
1.1 原始类型有哪些
原始类型通常包括:
numberstringbooleanundefinednullsymbolbigint
它们的共同特点可以抓这几条:
- 值本身更小、更直接
- 通常按值参与复制和比较
- 不像对象那样带有一组可随时增删的属性
1.2 引用类型主要是什么
工程里最常见的引用类型,本质上都可以归到 object 这条主线。
例如:
- 普通对象
- 数组
- 函数
- 日期对象
- 正则对象
Map、Set
它们的共同特点是:
- 更适合表达结构化数据
- 复制时更常见的是“复制引用关系”
- 比较时默认比较的不是内容本身,而是是否指向同一个对象
1.3 typeof、instanceof、Object.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]真正更稳的经验通常是:
- 判断原始类型,优先考虑
typeof - 判断数组、日期等对象类型,不要只靠
typeof - 需要跨场景更稳定地区分对象时,再考虑
Object.prototype.toString.call
2. 为什么 JavaScript 里既有原始类型,又有对象
这是因为它要同时解决两类问题:
- 用简单值表达最基础的数据
- 用对象表达更复杂的结构和行为
比如:
- 年龄、价格、开关状态,更适合用原始类型
- 用户信息、商品数据、组件状态,更适合用对象
3. undefined 和 null 到底怎么区分
这两个词很容易一起出现,但含义并不完全一样。
3.1 undefined
可以看成 有这个变量或属性位子,但当前还没有明确的值。
常见场景:
- 变量声明了但还没赋值
- 访问对象里不存在的属性
- 函数没有显式返回值
3.2 null
可以看成 这里明确表示“没有对象”或“当前故意置空”。
常见场景:
- 初始化时先占位
- 某个对象关系被主动清空
- 用来表达“此处本应是对象,但当前为空”
3.3 一句话区分
undefined更像“默认还没有值”null更像“明确写出来的空值”
4. 为什么对象比较经常和直觉不一样
看下面这个例子:
js
const a = { name: 'tom' }
const b = { name: 'tom' }
console.log(a === b) // false很多人第一次看到会觉得奇怪,因为内容明明一样。
真正原因是:对象比较默认比较的是引用是否相同,而不是结构内容是否一样。
也就是说:
a和b虽然内容一样- 但它们不是同一个对象实例
- 所以严格比较结果是
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这里不是“复制了一份完整对象”,而是:
user1 和 user2 指向了同一个对象。
6. 浅拷贝和深拷贝到底在解决什么问题
6.1 浅拷贝
浅拷贝可以看成 只复制当前这一层,里面嵌套的对象仍然共享原来的引用。
常见方式:
- 展开运算符
... Object.assign- 数组的
slice、concat
js
const a = {
name: 'tom',
address: {
city: 'shanghai'
}
}
const b = { ...a }
b.address.city = 'beijing'
console.log(a.address.city) // beijing6.2 深拷贝
深拷贝解决的是:希望得到一份真正独立的嵌套对象结构。
常见方式:
structuredClone- 自己递归实现
- 借助工具库
需要注意:
JSON.parse(JSON.stringify(...))只能算一种有限场景下的简化方案- 它会丢失
undefined、函数、symbol、日期对象等信息
7. 类型转换为什么容易出坑
JavaScript 的类型转换分两类:
- 显式转换
- 隐式转换
7.1 显式转换
这是代码明确表达“我要转类型”。
常见方式:
Number(value)String(value)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可以直接把它理解成:
+只要一边进入字符串语境,就更容易走拼接-、*、/更偏数值运算,所以会更积极地把两边转成数字
所以很多看似怪异的结果,本质上不是“转换随机”,而是:不同运算符期待的目标类型根本不一样。
7.5 真值和假值为什么也经常让人误判
在条件判断里,JavaScript 并不要求你一定写出 true 或 false。
很多值都会被自动转成布尔值。
常见假值主要包括:
false0-00n''nullundefinedNaN
例如:
js
if ('0') {
console.log('run')
}这段代码会执行,因为 '0' 是非空字符串,属于真值。
工程里一个很常见的问题就是:
- 很多人把“看起来像空”误当成假值
- 但
'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这些结果之所以怪,不是因为规则不存在,而是因为比较前发生了多轮类型转换:
- 布尔值比较时,可能先转数字
- 对象比较时,可能先转原始值
- 字符串和数字比较时,又会继续转数字
也就是说:== 的真正问题不是“偶尔不准”,而是它会把比较过程拉进一条很长的隐式转换链。`
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所以可以直接把它理解成:
- 日常比较大多还是用
=== - 需要更细分辨
NaN、+0、-0这类边界时,可以考虑Object.is
9. 一张快速对照表
| 维度 | 原始类型 | 引用类型 |
|---|---|---|
| 典型内容 | number、string、boolean 等 | 对象、数组、函数等 |
| 复制行为 | 更接近复制值本身 | 更常见是复制引用 |
| 比较行为 | 更常见按值比较 | 默认按引用比较 |
| 是否适合表达复杂结构 | 不适合 | 适合 |
| 常见问题 | 隐式转换、相等判断 | 共享引用、浅拷贝副作用 |
10. 工程上最容易踩的坑
10.1 误以为对象比较的是内容
对象默认比较的是引用。
10.2 误以为展开运算符就是深拷贝
它只会复制一层。
10.3 把 == 当成普通比较
它背后会触发类型转换,结果可能和直觉不一致。
10.4 误把 null 和 undefined 完全等同
它们都表示“空”,但语义和使用场景不一样。
11. 小结
这条主线如果压缩成几句话,可以记住:
- JavaScript 里的值大体分成原始类型和引用类型
- 原始类型更像直接值,对象更像结构化引用
- 对象比较默认看引用,不看内容
- 很多奇怪结果都和隐式类型转换有关
下一步如果继续往下读,最自然的是进入 函数、作用域、this 与闭包,因为函数执行时“看到了什么”,和这里的值模型是直接连着的。