一、内存两大区域:栈内存 Stack、堆内存 Heap
计算机给 JS 划分两块地方存放数据,分工完全不一样。
- 栈内存(Stack)
空间小,读写速度快
存放两类东西:
基本类型的真实值
引用类型的内存地址(编号),不是对象本身!
栈的变量生命周期简单:函数执行完毕、变量失效,直接出栈销毁。
- 堆内存(Heap)
空间很大,读写速度比栈慢
只存引用类型真正的数据:对象、数组,一大坨键值对。
最最重要一句话:
引用类型的对象本体永远待在堆;栈里面的变量,只拿着堆的地址编号,相当于门牌号。
举个例子代码:
js
运行
let user = { name: "小明", age: 18 }
内存发生的事情:
JS 在堆内存开辟一块空间,存入完整对象 { name:"小明", age:18 },假设这块堆的地址编号:0xaa01
在栈内存创建变量 user,变量 user 里面不存对象,只存地址 0xaa01
当写 user.name:JS 读取栈中 user 的地址 0xaa01,拿着地址去堆里面找到真实对象,读取 name 属性。
形象比喻:
堆 = 一套真实房子;栈变量 = 写着房子门牌号的小纸条。
user 手里只有纸条(地址),房子本体在堆。访问属性就是拿着纸条去找房子。
二、基本类型 vs 引用类型赋值本质区别
- 基本类型:赋值 = 复制一份真实的值(完全拷贝)
js
运行
let a = 100
let b = a
b = 200
console.log(a) // 100
内存过程:
栈:a = 100
let b = a:把 100 这个值复制一份给 b,栈里出现两份独立数据
修改 b=200,只改动 b 自己的数据,a 不受任何影响。
两个完全独立副本,井水不犯河水。
- 引用类型:赋值 = 复制地址纸条!不会复制堆里面的对象!
js
运行
let u1 = { name: "小明" }
let u2 = u1
u2.name = "小红"
console.log(u1.name) // 小红
// ② 给变量直接赋全新对象:给u2换一张新门牌号,断开和u1的联系
u2 = { name: "小李" }
①:改房子里面家具 → 所有人看见变化
②:直接换一套新房子,旧房子不受影响。
三、浅拷贝、深拷贝:如何真正产生新对象
需求:不想共用同一个堆对象,想要两个独立对象。
浅拷贝:只复制对象第一层,生成新堆对象;嵌套对象依旧共用地址
js
运行
let obj1 = { a: 10, info: { age: 18 } }
// 浅拷贝写法1:展开运算符
let obj2 = { ...obj1 }
// 浅拷贝写法2 Object.assign
// let obj2 = Object.assign({}, obj1)
内存行为:
堆开辟新对象给 obj2,复制第一层 a:10;
但是里面的 info 是对象,只复制 info 的地址,info 对象还是原来堆里那一个。
js
运行
obj2.a = 999
console.log(obj1.a) //10 第一层互不干扰
obj2.info.age = 999
console.log(obj1.info.age) //999 嵌套对象还是共享堆
浅拷贝只能解决一层,嵌套引用依旧存在。
深拷贝:所有层级全部新建堆对象,彻底切断引用
简单快速方案(局限性:不能拷贝函数、undefined、symbol)
js
运行
let obj2 = JSON.parse(JSON.stringify(obj1))