第二章:内存模型
1. 它是什么
你可以先把这一章理解成两个核心问题:
变量里的值到底是怎么存的为什么有的赋值互不影响,有的会互相影响面试里常出现的几个词:
- 栈内存
- 堆内存
- 值拷贝
- 引用拷贝
- 浅拷贝
- 深拷贝
- 垃圾回收
- 内存泄漏
这一整章其实就在回答一句话:JS 里的数据到底放在哪,变量之间为什么会相互影响或不影响。
2. 为什么这样设计
因为程序运行时,不可能所有数据都用同一种方式存储。
简单值 比如:
let a = 1
let b = 'hello'这种值很简单、很小,直接处理就行。
复杂值
比如:
let user = {
name: 'Tom',
age: 18
}这种值里面可能嵌套很多层:
let data = {
user: {
profile: {
name: 'Tom'
}
},
list: [1, 2, 3]
}如果每次赋值都把整个对象完整复制一份,代价太大。
所以才会有这种设计思路:
- 简单值:直接保存、直接复制
- 复杂对象:保存“引用”,真正内容放在另一块更适合存对象的内存区域
这就是为什么会出现“对象赋值会互相影响”。
3. 底层怎么运行
这里是本章核心。
3.1 栈内存和堆内存是什么
先给你一个面试够用版理解:
栈(stack):通常存变量标识、执行上下文、调用关系、一些直接值堆(heap):通常存对象、数组、函数等复杂数据的实际内容 例如:
let a = 10
let obj = { name: 'Tom' }你可以粗略理解成:
a这个变量关联值10obj这个变量里保存的是一个引用- 真正的
{ name: 'Tom' }对象内容在堆里 但是要注意一句非常重要的话“基本类型一定在栈,对象一定在堆”是帮助理解的工程说法,不是 JS 规范强制规定。更严谨一点说: - JS 规范不规定底层内存一定怎么放
- 具体怎么实现,由 V8 等 JS 引擎决定
- 但面试里,用“栈存变量、堆存对象”来解释是可以的
这个你要记住,后面答题会显得很稳。
3.2 什么叫值拷贝
看代码:
let a = 1
let b = a
b = 2
console.log(a) // 1
console.log(b) // 2这里发生了什么?
a的值是1b = a时,复制的是1这个值本身- 后面你改
b,不会影响a
这就叫:基本类型赋值时,一般表现为值拷贝
也就是: - 复制的是数据本身
- 两个变量之间没有共享关系
3.3 什么叫引用拷贝
再看对象:
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 为什么“修改属性”会互相影响,但“重新赋值”不一定会
这个很关键,面试很爱追问。
看第一种:
let a = { name: 'Tom' }
let b = a
b.name = 'Jack'
console.log(a.name) // Jack这里改的是:同一个对象里的属性
所以会互相影响。
再看第二种:
let a = { name: 'Tom' }
let b = a
b = { name: 'Jack' }
console.log(a.name) // Tom
console.log(b.name) // Jack这里不是改原对象属性,
而是让 b 重新引用了一个新对象。
所以:
- 改共享对象内部属性:会互相影响
- 让变量重新指向新对象:不一定影响原变量
这点你一定要分清。
4. 浅拷贝和深拷贝
这块是第二章最核心的面试点之一。
4.1 浅拷贝是什么
浅拷贝的意思是:只复制对象的第一层属性 如果属性值还是对象,那么复制的仍然是那个对象的引用。
例如:
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()
例如:
const a = { x: 1, y: { z: 2 } }
const b = Object.assign({}, a)这里 b.y 和 a.y 还是同一个对象。
4.3 深拷贝是什么
深拷贝的意思是:递归复制对象的每一层,生成一个完全独立的新对象 例如:
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 - 不能处理循环引用
Date、RegExp会有问题Map、Set不行
例如:
const obj = {
a: undefined,
b: function () {},
c: Symbol('x')
}
console.log(JSON.parse(JSON.stringify(obj))) // {}所以你面试里不要把它说成“万能深拷贝”。
方式 2:structuredClone() 现代 JS 提供了更正规的深拷贝能力:
const newObj = structuredClone(oldObj)它比 JSON 方案强很多,但也不是所有类型都支持。
方式 3:手写递归深拷贝
面试常考。核心思想就是:
- 判断当前值是不是对象
- 递归复制每个属性
- 处理数组和对象
- 进一步可处理循环引用
5. 垃圾回收是什么
它是什么 垃圾回收就是:程序自动回收那些已经无法再访问到的内存
因为如果不用的对象一直留着,内存会越来越满。
为什么这样设计 因为 JS 是高级语言,不希望开发者像 C/C++ 那样手动管理内存。
否则前端开发成本会很高,也容易出错。
底层怎么运行 你先记住一个最常见说法:如果一个对象没有任何地方再能访问到它,它就可能被垃圾回收。 最常见的理解模型叫:
- 可达性
- 标记清除 比如:
let obj = { name: 'Tom' }
obj = null原来那个对象如果没有其他变量再引用它,就成了“不可达对象”,后面就可能被 GC 回收。
实际开发怎么用 你不需要手动“删除内存”,但你要避免:
- 大对象长期被引用
- 定时器不清除
- 事件监听不移除
- 闭包把无用数据一直留住
6. 内存泄漏是什么
它是什么 不是“内存没了”,而是:本来应该释放的内存,因为仍然被引用着,导致无法回收
常见场景
- 全局变量滥用
data = new Array(1000000)如果不小心挂到全局,就可能一直不释放。
2. 定时器没清掉
setInterval(() => {
console.log('run')
}, 1000)如果页面不用了,定时器还在跑,就会持续占资源。
3. 事件监听没移除
window.addEventListener('resize', handler)组件销毁了但监听没移除,也可能泄漏。
4. 闭包引用了大对象
后面讲闭包时你会更清楚。
7. 面试高频题:栈内存和堆内存的区别
这是你最开始就提到的问题,我给你标准答法。
标准回答通常可以把 JavaScript 的内存分为栈和堆来理解。栈主要用于保存执行上下文、函数调用关系以及部分直接值,堆主要用于存放对象、数组、函数等复杂数据。基本类型赋值时通常表现为值拷贝,而对象赋值时复制的是引用,因此多个变量可能共享同一个对象。需要注意的是,栈和堆这种说法更多是工程上的理解模型,ECMAScript 规范本身并没有强制规定具体存储方式,最终实现取决于 JS 引擎。
这个回答已经比较成熟了。
8. 面试高频题:浅拷贝和深拷贝的区别
标准回答浅拷贝只复制对象的第一层属性,如果属性值还是对象,那么复制的仍然是引用,所以修改深层属性时会相互影响。深拷贝会递归复制对象的每一层,生成完全独立的新对象,修改副本不会影响原对象。常见浅拷贝方式有扩展运算符和 Object.assign,常见深拷贝方式有 structuredClone、手写递归等。JSON.parse(JSON.stringify()) 也能处理部分场景,但有明显局限。
9. 这一章你要真正吃透的 6 句话
- 对象变量里拿到的通常不是对象本体,而是对象的引用。
- 基本类型赋值通常表现为值拷贝。
- 对象赋值通常表现为引用拷贝。
- 浅拷贝只复制第一层,深层对象仍然共享。
- 深拷贝会递归复制每一层。
- 垃圾回收回收的是不可达对象,不是你“手动删除”的对象。
10. 给你几个立即判断的例子
你自己先想答案,不要急着看解释。
题 1
let a = 1
let b = a
b = 2
console.log(a)答案:1 因为基本类型值拷贝。
题 2
let a = { x: 1 }
let b = a
b.x = 2
console.log(a.x)答案:2 因为共享同一个对象。
题 3
let a = { x: 1 }
let b = a
b = { x: 2 }
console.log(a.x)答案:1
因为这里是让 b 重新引用新对象,不是修改原对象。
题 4
const a = { info: { age: 18 } }
const b = { ...a }
b.info.age = 20
console.log(a.info.age)答案:20
因为展开运算符对嵌套对象是浅拷贝。
11. 这章你先自己答这 6 题
你按你自己的话回答,我再帮你纠偏:
- 栈内存和堆内存分别是干什么的?
通常可以把栈理解为保存执行上下文、函数调用关系和部分直接值的区域,把堆理解为保存对象、数组、函数等复杂数据内容的区域。基本类型赋值通常表现为值拷贝,而对象赋值通常复制的是引用,因此多个变量可能引用同一个对象。栈和堆是工程上的理解模型,具体实现由 JS 引擎决定。
- 为什么对象赋值后修改一个,另一个可能会变?
因为对象赋值复制的是引用,两个变量可能引用同一个对象,所以修改其中一个变量访问到的对象属性,本质上是在修改同一个对象,另一个变量也会看到变化。
- 修改对象属性和给变量重新赋值,有什么区别?
修改对象属性,改变的是共享对象内部的数据,只要多个变量引用同一个对象,就都会受到影响;而给变量重新赋值,是让这个变量重新绑定到一个新的值或新对象,不会直接修改原来那个共享对象。
什么是浅拷贝?什么是深拷贝?
浅拷贝只复制对象的第一层属性,如果属性值还是对象,那么复制的仍然是引用,所以深层属性可能仍然共享。深拷贝会递归复制对象的每一层,生成一个完全独立的新对象,修改副本不会影响原对象。
- Object.assign() 和 ... 是深拷贝还是浅拷贝?
它们只对第一层做复制,如果属性值还是引用类型,复制的仍然是引用。
- 什么是内存泄漏?常见场景有哪些?
内存泄漏指的是本来应该被垃圾回收释放的内存,因为仍然存在无用引用而无法回收,导致内存持续占用。常见场景包括未清理的定时器、未移除的事件监听、闭包长期持有大对象引用、以及不必要的全局变量。
