Skip to content

第二章:内存模型

1. 它是什么

你可以先把这一章理解成两个核心问题:

  1. 变量里的值到底是怎么存的
  2. 为什么有的赋值互不影响,有的会互相影响 面试里常出现的几个词:
  • 栈内存
  • 堆内存
  • 值拷贝
  • 引用拷贝
  • 浅拷贝
  • 深拷贝
  • 垃圾回收
  • 内存泄漏
    这一整章其实就在回答一句话:
    JS 里的数据到底放在哪,变量之间为什么会相互影响或不影响。

2. 为什么这样设计

因为程序运行时,不可能所有数据都用同一种方式存储。
简单值 比如:

js
let a = 1
let b = 'hello'

这种值很简单、很小,直接处理就行。
复杂值
比如:

js
let user = {
  name: 'Tom',
  age: 18
}

这种值里面可能嵌套很多层:

js
let data = {
  user: {
    profile: {
      name: 'Tom'
    }
  },
  list: [1, 2, 3]
}

如果每次赋值都把整个对象完整复制一份,代价太大。
所以才会有这种设计思路:

  • 简单值:直接保存、直接复制
  • 复杂对象:保存“引用”,真正内容放在另一块更适合存对象的内存区域
    这就是为什么会出现“对象赋值会互相影响”。

3. 底层怎么运行

这里是本章核心。

3.1 栈内存和堆内存是什么

先给你一个面试够用版理解:

  • 栈(stack):通常存变量标识、执行上下文、调用关系、一些直接值
  • 堆(heap):通常存对象、数组、函数等复杂数据的实际内容 例如:
js
let a = 10
let obj = { name: 'Tom' }

你可以粗略理解成:

  • a 这个变量关联值 10
  • obj 这个变量里保存的是一个引用
  • 真正的 { name: 'Tom' } 对象内容在堆里 但是要注意一句非常重要的话“基本类型一定在栈,对象一定在堆”是帮助理解的工程说法,不是 JS 规范强制规定。 更严谨一点说:
  • JS 规范不规定底层内存一定怎么放
  • 具体怎么实现,由 V8 等 JS 引擎决定
  • 但面试里,用“栈存变量、堆存对象”来解释是可以的

这个你要记住,后面答题会显得很稳。

3.2 什么叫值拷贝

看代码:

js
let a = 1
let b = a
b = 2

console.log(a) // 1
console.log(b) // 2

这里发生了什么?

  • a 的值是 1
  • b = a 时,复制的是 1 这个值本身
  • 后面你改 b,不会影响 a
    这就叫:
    基本类型赋值时,一般表现为值拷贝
    也就是:
  • 复制的是数据本身
  • 两个变量之间没有共享关系

3.3 什么叫引用拷贝

再看对象:

js
let obj1 = { name: 'Tom' }
let obj2 = obj1

obj2.name = 'Jack'

console.log(obj1.name) // Jack
console.log(obj2.name) // Jack

这里不是复制了一份新对象。
而是:

  • obj1 引用这个对象
  • obj2 = obj1 后,obj2 也引用同一个对象
  • 所以改 obj2.name,本质是在改那一个共享对象
    这就叫:
    对象赋值时,复制的通常是引用,不是对象本体
    注意这里最好别老说“指针”,面试里更推荐说:
    引用
    地址引用
    指向同一个对象

3.4 为什么“修改属性”会互相影响,但“重新赋值”不一定会

这个很关键,面试很爱追问。
看第一种:

js
let a = { name: 'Tom' }
let b = a

b.name = 'Jack'

console.log(a.name) // Jack

这里改的是:同一个对象里的属性
所以会互相影响。
再看第二种:

js
let a = { name: 'Tom' }
let b = a

b = { name: 'Jack' }

console.log(a.name) // Tom
console.log(b.name) // Jack

这里不是改原对象属性,
而是让 b 重新引用了一个新对象。
所以:

  • 改共享对象内部属性:会互相影响
  • 让变量重新指向新对象:不一定影响原变量
    这点你一定要分清。

4. 浅拷贝和深拷贝

这块是第二章最核心的面试点之一。

4.1 浅拷贝是什么

浅拷贝的意思是:
只复制对象的第一层属性 如果属性值还是对象,那么复制的仍然是那个对象的引用。
例如:

js
const obj1 = {
  name: 'Tom',
  info: {
    age: 18
  }
}

const obj2 = { ...obj1 }
obj2.name = 'Jack'
obj2.info.age = 20

console.log(obj1.name) // Tom
console.log(obj1.info.age) // 20

为什么 name 没影响,info.age 却影响了?
因为:

  • name 是第一层基本类型,复制后互不影响
  • info 是对象,浅拷贝只复制了它的引用
    所以: 浅拷贝只断开第一层,深层对象仍然共享

4.2 常见浅拷贝方式

常见的有:

  • 展开运算符 ...
  • Object.assign()
  • 数组的 slice()
  • 数组的 concat()

例如:

js
const a = { x: 1, y: { z: 2 } }
const b = Object.assign({}, a)

这里 b.ya.y 还是同一个对象。

4.3 深拷贝是什么

深拷贝的意思是:
递归复制对象的每一层,生成一个完全独立的新对象 例如:

js
const obj1 = {
  name: 'Tom',
  info: {
    age: 18
  }
}

const obj2 = deepClone(obj1)
obj2.info.age = 20

console.log(obj1.info.age) // 18

这里修改 obj2 不影响 obj1,因为深层也复制开了。

4.4 常见深拷贝方式

方式 1:JSON.parse(JSON.stringify(obj)) 优点:

  • 简单
  • 面试里容易写

缺点很明显:

  • 不能拷贝 function
  • 不能处理 undefined
  • 不能处理 symbol
  • 不能处理循环引用
  • DateRegExp 会有问题
  • MapSet 不行
    例如:
js
const obj = {
  a: undefined,
  b: function () {},
  c: Symbol('x')
}

console.log(JSON.parse(JSON.stringify(obj))) // {}

所以你面试里不要把它说成“万能深拷贝”。

方式 2:structuredClone() 现代 JS 提供了更正规的深拷贝能力:

js
const newObj = structuredClone(oldObj)

它比 JSON 方案强很多,但也不是所有类型都支持。
方式 3:手写递归深拷贝
面试常考。核心思想就是:

  • 判断当前值是不是对象
  • 递归复制每个属性
  • 处理数组和对象
  • 进一步可处理循环引用

5. 垃圾回收是什么

它是什么 垃圾回收就是:
程序自动回收那些已经无法再访问到的内存
因为如果不用的对象一直留着,内存会越来越满。
为什么这样设计 因为 JS 是高级语言,不希望开发者像 C/C++ 那样手动管理内存。
否则前端开发成本会很高,也容易出错。
底层怎么运行 你先记住一个最常见说法:
如果一个对象没有任何地方再能访问到它,它就可能被垃圾回收。 最常见的理解模型叫:

  • 可达性
  • 标记清除 比如:
js
let obj = { name: 'Tom' }
obj = null

原来那个对象如果没有其他变量再引用它,就成了“不可达对象”,后面就可能被 GC 回收。

实际开发怎么用 你不需要手动“删除内存”,但你要避免:

  • 大对象长期被引用
  • 定时器不清除
  • 事件监听不移除
  • 闭包把无用数据一直留住

6. 内存泄漏是什么

它是什么 不是“内存没了”,而是:
本来应该释放的内存,因为仍然被引用着,导致无法回收
常见场景

  1. 全局变量滥用
js
data = new Array(1000000)

如果不小心挂到全局,就可能一直不释放。
2. 定时器没清掉

js
setInterval(() => {
  console.log('run')
}, 1000)

如果页面不用了,定时器还在跑,就会持续占资源。
3. 事件监听没移除

js
window.addEventListener('resize', handler)

组件销毁了但监听没移除,也可能泄漏。
4. 闭包引用了大对象
后面讲闭包时你会更清楚。

7. 面试高频题:栈内存和堆内存的区别

这是你最开始就提到的问题,我给你标准答法。
标准回答
通常可以把 JavaScript 的内存分为栈和堆来理解。栈主要用于保存执行上下文、函数调用关系以及部分直接值,堆主要用于存放对象、数组、函数等复杂数据。基本类型赋值时通常表现为值拷贝,而对象赋值时复制的是引用,因此多个变量可能共享同一个对象。需要注意的是,栈和堆这种说法更多是工程上的理解模型,ECMAScript 规范本身并没有强制规定具体存储方式,最终实现取决于 JS 引擎。

这个回答已经比较成熟了。

8. 面试高频题:浅拷贝和深拷贝的区别

标准回答
浅拷贝只复制对象的第一层属性,如果属性值还是对象,那么复制的仍然是引用,所以修改深层属性时会相互影响。深拷贝会递归复制对象的每一层,生成完全独立的新对象,修改副本不会影响原对象。常见浅拷贝方式有扩展运算符和 Object.assign,常见深拷贝方式有 structuredClone、手写递归等。JSON.parse(JSON.stringify()) 也能处理部分场景,但有明显局限。

9. 这一章你要真正吃透的 6 句话

  1. 对象变量里拿到的通常不是对象本体,而是对象的引用。
  2. 基本类型赋值通常表现为值拷贝。
  3. 对象赋值通常表现为引用拷贝。
  4. 浅拷贝只复制第一层,深层对象仍然共享。
  5. 深拷贝会递归复制每一层。
  6. 垃圾回收回收的是不可达对象,不是你“手动删除”的对象。

10. 给你几个立即判断的例子

你自己先想答案,不要急着看解释。
题 1

js
let a = 1
let b = a
b = 2
console.log(a)

答案:1 因为基本类型值拷贝。
题 2

js
let a = { x: 1 }
let b = a
b.x = 2
console.log(a.x)

答案:2 因为共享同一个对象。
题 3

js
let a = { x: 1 }
let b = a
b = { x: 2 }
console.log(a.x)

答案:1
因为这里是让 b 重新引用新对象,不是修改原对象。
题 4

js
const a = { info: { age: 18 } }
const b = { ...a }
b.info.age = 20
console.log(a.info.age)

答案:20
因为展开运算符对嵌套对象是浅拷贝。

11. 这章你先自己答这 6 题

你按你自己的话回答,我再帮你纠偏:

  1. 栈内存和堆内存分别是干什么的?

通常可以把栈理解为保存执行上下文、函数调用关系和部分直接值的区域,把堆理解为保存对象、数组、函数等复杂数据内容的区域。基本类型赋值通常表现为值拷贝,而对象赋值通常复制的是引用,因此多个变量可能引用同一个对象。栈和堆是工程上的理解模型,具体实现由 JS 引擎决定。

  1. 为什么对象赋值后修改一个,另一个可能会变?

因为对象赋值复制的是引用,两个变量可能引用同一个对象,所以修改其中一个变量访问到的对象属性,本质上是在修改同一个对象,另一个变量也会看到变化。

  1. 修改对象属性和给变量重新赋值,有什么区别?

修改对象属性,改变的是共享对象内部的数据,只要多个变量引用同一个对象,就都会受到影响;而给变量重新赋值,是让这个变量重新绑定到一个新的值或新对象,不会直接修改原来那个共享对象。

什么是浅拷贝?什么是深拷贝?

浅拷贝只复制对象的第一层属性,如果属性值还是对象,那么复制的仍然是引用,所以深层属性可能仍然共享。深拷贝会递归复制对象的每一层,生成一个完全独立的新对象,修改副本不会影响原对象。

  1. Object.assign() 和 ... 是深拷贝还是浅拷贝?

它们只对第一层做复制,如果属性值还是引用类型,复制的仍然是引用。

  1. 什么是内存泄漏?常见场景有哪些?

内存泄漏指的是本来应该被垃圾回收释放的内存,因为仍然存在无用引用而无法回收,导致内存持续占用。常见场景包括未清理的定时器、未移除的事件监听、闭包长期持有大对象引用、以及不必要的全局变量。