JavaScript  原始类型 vs 对象的所有误区、不可变性、按共享传递、拷贝、引用、V8 存储原理,是 JS 最核心的底层基础。


一、开篇直击经典问题

const original = { name: "Alice" }
const copy = original
copy.name = "Bob"
console.log(original.name) // "Bob"

为什么改拷贝会影响原对象?答案在于:原始类型按值独立,对象按引用共享

破除流传最广的谣言 ❌

表格

常见谣言 真实情况
“原始类型在栈上,对象在堆上” 这是实现细节,不是标准,且并不完全正确
“原始类型按值传递,对象按引用传递” JS 统一使用 按共享传递(call by sharing)
“值类型 vs 引用类型” ES 标准只定义:原始类型、对象
“const 让对象不可变” const 只禁止重新赋值,不禁止修改内容

二、标准定义:只有两类值

ECMAScript 标准只区分两种数据:

1. 原始类型 Primitive

  • string
  • number
  • bigint
  • boolean
  • undefined
  • null
  • symbol

2. 对象 Object

  • 普通对象 {}
  • 数组 []
  • 函数
  • Date / Map / Set 等

真正的区别只有一个:不可变性

  • 原始类型:不可变(immutable)
  • 对象:可变(mutable)

三、行为对比

原始类型:不可变、独立

  • 无法修改本身,只能替换
  • 拷贝后互不影响
  • 比较
let a = "hello"
let b = a
b = "world"
console.log(a) // "hello" 不受影响

对象:可变、共享

  • 可以修改内部属性
  • 赋值时拷贝引用(指针)
  • 多个变量指向同一个对象
  • 引用身份比较
let a = { name: "A" }
let b = a
b.name = "B"
console.log(a.name) // "B" 被影响

四、房子钥匙类比(最易懂)

  • 原始类型 = 便签复制一张便签,两张完全独立,改一张不影响另一张。

  • 对象 = 房子与钥匙复制钥匙,两把钥匙开同一栋房子。任何人改动房子,所有人都能看到变化。


五、JS 真正的传参方式:按共享传递 Call-by-Sharing

这是前端面试最高频底层题

规则:

  1. 函数传参永远拷贝引用(钥匙)
  2. 修改属性( mutation)会影响原对象
  3. 重新赋值参数不会影响原变量

1)修改属性 → 影响外部 ✅

function rename(person) {
  person.name = "Bob"
}
const user = { name: "Alice" }
rename(user)
console.log(user.name) // "Bob"

2)重新赋值参数 → 不影响外部 ❌

function replace(person) {
  person = { name: "Charlie" }
}
const user = { name: "Alice" }
replace(user)
console.log(user.name) // "Alice" 不变

六、比较规则

原始类型:按值比较

"hello" === "hello" // true
10 === 10 // true

对象:按引用身份比较

{} === {} // false
[] === [] // false
{name:"A"} === {name:"A"} // false

只有完全同一个对象才相等。


七、修改 vs 重新赋值 Mutation vs Reassignment

修改(Mutation)

改变对象内部内容:

obj.name = "Bob"
arr.push(1)
arr[0] = 99

重新赋值(Reassignment)

让变量指向新值:

obj = {}
arr = []

const 的真相

  • const 禁止重新赋值
  • const 不禁止修改对象 / 数组
const arr = [1,2,3]
arr.push(4) // ✅ 允许
arr = [] // ❌ 报错

想真正不可变?用 Object.freeze()(浅冻结)。


八、浅拷贝 vs 深拷贝

浅拷贝 Shallow Copy

只拷贝第一层,嵌套对象仍共享。

const copy = { ...obj }
const copy = Object.assign({}, obj)
const arrCopy = [...arr]

深拷贝 Deep Copy

所有层级完全独立,用:

const deepCopy = structuredClone(original)

九、V8 引擎真实存储方式

栈 / 堆模型是简化模型,不完全正确

V8 真实规则:

  1. 只有小整数(Smi)直接存
  2. 字符串、大数、BigInt、对象 → 都存在堆
  3. 变量保存的是指针 / 引用

十、高频 BUG 合集

  1. 拷贝数组却共享 → 用 [...arr] / slice()
  2. 浅拷贝嵌套对象被改 → 用 structuredClone
  3. {} === {} 为 false → 按引用比较
  4. 函数内改参数影响外部 → 先拷贝再改
  5. 以为 const 让对象不可变 → 用 Object.freeze
  6. sort 会改变原数组 → 先拷贝 [...nums].sort()

十一、核心结论(必须背)

  1. JS 只有原始类型对象
  2. 原始类型不可变,对象可变
  3. 所有值传参都是 按共享传递
  4. 修改属性影响外部,重新赋值不影响
  5. 对象按引用比较,原始按比较
  6. const 只限制赋值,不限制修改
  7. 浅拷贝共享嵌套,深拷贝完全独立

Symbols:原始类型中的唯一例外

Symbol 是原始类型,但拥有身份标识。即使描述文字一样,两个 Symbol 也永不相等。

const sym1 = Symbol("id")
const sym2 = Symbol("id")
sym1 === sym2 // false

修改 (Mutation) vs 重新赋值 (Reassignment)

修改(Mutation)

直接改变对象内部内容,不改变引用。

arr.push(4)
obj.name = "Bob"
delete obj.age

重新赋值(Reassignment)

让变量指向一个全新的值。

arr = [1,2,3]
obj = {}
a = 100

const 的巨大误区

const 只做一件事:禁止变量重新赋值

但它 完全不禁止对象 / 数组修改

const user = { name: "Alice" }
user.name = "Bob" // ✅ 允许
user = {} // ❌ 报错

若要真正不可变,必须使用:Object.freeze ()(浅冻结)

const user = Object.freeze({ name: "Alice" })
user.name = "Bob" // 无效

注意:freeze 是浅冻结,嵌套对象依然能改。


浅拷贝 vs 深拷贝(完整版)

浅拷贝

只复制第一层,嵌套对象仍然共享。

方法:

  • { ...obj }
  • Object.assign({}, obj)
  • [...arr]
  • arr.slice()

问题:

const copy = { ...user }
copy.address.city = "XXX" // 原对象也会变

深拷贝

复制所有层级,完全独立。

现代标准方法:

const deepCopy = structuredClone(original)

支持:

  • 对象 / 数组
  • Date / Map / Set
  • 循环引用
  • 不支持:函数、Symbol

旧式替代:

JSON.parse(JSON.stringify(obj))

缺点:丢失 Date、undefined、函数、循环引用。


V8 引擎如何真正存储值(破除栈 / 堆谣言)

谣言:

“原始类型存在栈里,对象存在堆里。”

事实(V8 真实机制):

  1. 只有小整数 Smi 直接存储-2³¹ ~ 2³¹-1

  2. 其他所有东西都在堆里

    • 字符串
    • 长数字
    • BigInt
    • 对象、数组、函数
    • 布尔、null、undefined 内部也是堆对象
  3. 变量保存的是 指针(引用)

所以:字符串绝对不在栈上!它是动态长度,一定存在堆。


JavaScript 最常见的 6 大坑

1. 数组赋值后互相影响

const a = [1,2,3]
const b = a
b.push(4)
// a 也变了

✅ 解决:拷贝

const b = [...a]

2. 函数内修改参数影响外部

function f(arr) {
  arr.push(99)
}

✅ 解决:先复制

const newArr = [...arr]

3. 浅拷贝导致嵌套对象被改

const copy = { ...user }
copy.address.city = "LA" // 原对象也变

✅ 解决:深拷贝

structuredClone(user)

4. 相信 {} === []

{} === {} // false
[] === [] // false

✅ 解决:比较内容


5. 以为 const 让对象不可变

const user = {}
user.name = "xx" // 依然可以改

✅ 解决:Object.freeze ()


6. sort 会修改原数组

const sorted = arr.sort()

✅ 解决:

const sorted = [...arr].sort()

最佳实践(必读)

  1. 尽量把对象当不可变使用
  2. 优先 const,减少意外赋值
  3. 修改数组前先拷贝
  4. 嵌套对象用 structuredClone
  5. 函数参数不要直接修改
  6. 记住:对象按引用比较

最终核心总结(面试必背)

  1. JavaScript 只有原始类型对象
  2. 本质区别:原始不可变,对象可变
  3. JS 传参一律是 按共享传递
  4. 修改属性会影响原对象;重新赋值不会
  5. 对象比较的是引用,不是内容
  6. const 不保证不可变,只禁止赋值
  7. 浅拷贝共享嵌套,深拷贝完全独立
  8. 栈 / 堆是简化模型,V8 只有小整数直接存储

🧩 新概念 + 类似概念 汇总

1. 核心基础概念

  • Primitive(原始类型)同类:值类型、基础类型、原子类型
  • Object(对象)同类:引用类型、复合类型、结构类型
  • Immutability(不可变性)同类:只读、不可修改、只能替换
  • Mutability(可变性)同类:可修改、可更新、原地变更

2. 赋值与传递

  • Call-by-Sharing(按共享传递)同类:按对象传递、拷贝引用、共享指针
  • Call-by-Value(按值传递)同类:值拷贝、独立副本
  • Call-by-Reference(按引用传递)同类:别名、指针传递(JS 不使用)
  • Reference(引用)同类:指针、地址、指向堆内存

3. 拷贝与克隆

  • Shallow Copy(浅拷贝)同类:一层拷贝、嵌套共享
  • Deep Copy(深拷贝)同类:完全克隆、全层级独立
  • structuredClone同类:原生深拷贝、支持循环引用

4. 变量与赋值

  • Reassignment(重新赋值)同类:修改变量指向
  • Mutation(修改 / 突变)同类:原地修改内容、属性变更
  • const 限定同类:禁止重新赋值、不保证不可变

5. 比较规则

  • Value Equality(值相等)同类:内容相同即相等
  • Reference Equality(引用相等)同类:同一个对象才相等、身份相等

6. 引擎底层

  • Heap(堆)同类:动态内存、复杂数据存储
  • Stack(栈)同类:简易存储模型(非真实机制)
  • Smi(Small Integer)同类:V8 小整数、直接存储
  • Pointer Tagging(指针标记)同类:V8 内部值存储优化

7. 可变性与不可变性

  • Immutability(不可变性)同类:只读、不可修改、纯值、无副作用
  • Mutation(修改 / 突变)同类:原地修改、更新内容
  • Reassignment(重新赋值)同类:修改变量指向、覆盖绑定

8. 函数传参机制

  • Call-by-Sharing(按共享传递)同类:按对象传递、拷贝引用
  • Call-by-Value(按值传递)同类:值副本、独立拷贝
  • Call-by-Reference(按引用传递)同类:别名、指针(JS 不使用)

9. 引用与身份

  • Reference(引用)同类:指针、内存地址、对象索引
  • Identity(对象身份)同类:引用相等、同一对象
  • Reference Equality(引用相等)同类:=== 比较对象

10. 不可变 API

  • Object.freeze()同类:浅冻结、只读对象、防篡改
  • Deep Freeze(深冻结)同类:递归冻结、完全不可变

11. 常见误区概念

  • Global Pollution(全局污染)同类:全局变量冲突
  • Side Effect(副作用)同类:意外修改外部状态
  • Circular Reference(循环引用)同类:互相引用、结构体循环

更多推荐