作为前端了解栈与堆是非常必要的,如果不能充分理解那么js的深拷贝、浅拷贝就没办法正确使用。
当然如果你是大学计算机专业相信你因该了解很透彻了,如果文章有不足之处请多多指教
一、栈与堆概念
栈(stack):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;(通俗讲:栈自动分配的内存空间,它由系统自动释放)
堆(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。(通俗讲:堆动态分配的内存,大小也不一定会自动释放)
说到这就不得不提js数据类型
二、js数据类型
基本数据类型: Number、String、Boolean、Null、 Undefined、Symbol(ES6),这些类型可以直接操作保存在变量中的实际值。
引用类型: Function,Array,Object,当我们需要访问这三种引用类型的值时,首先得从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。
三、栈与堆区别
1、缓存方式区别
1).栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2).堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
2、堆栈数据结构区别
1).栈(数据结构):一种先进后出的数据结构。
2).堆(数据结构):堆可以被看成是一棵树,如:堆排序;
了解完栈与堆,我们来说说浅拷贝与深拷贝;上面我们介绍的数据的基本类型
四、基本数据类型(存放在栈中)
基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问。基本数据类型的值是没有办法添加属性和方法的
案例一
let a = 10;
let b = a;
b = 20;
console.log(a); // 10值
console.log(b); // 20值
解释:let a = 10;let b =a;
是 a 先取出 10,copy 一份放到 b 里面,改变 a 的值,b 的值是不变的,再把 a=20;时 b 的值还是 10,不发生改变
案例二
let arr = [1,2];
let arr1 =arr;
arr.push(3);
console.log(arr) // [1,2,3]
console.log(arr1) // [1,2,3]
解释:引用值是在栈内存里面放堆的地址,拷贝的也是地址,所以改变 arr,arr1 也变了,在栈中arr,arr1同时指向一个堆,
let arr = [1,2]; let arr1 =arr; arr = [1,3]; document.write(arr1)
arr = [1,3];是新建了一个新的房间。arr1 是 1,2,现在是插入新引入值”房间”,
会在堆里面重新申请一间房,并指向新房间
五、深拷贝
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会影响到原对象。
1、数组的深拷贝:
slice(slice()操作数组时,不会对原数组有影响,会产出一个新的数组。)
let arr1 = [1, 42, 5, 6]
let arr2 = arr1.slice()
arr2[0] = 100
console.log(arr1) // [1, 42, 5, 6]
console.log(arr2) // [100, 42, 5, 6]
concat (concat()方法,能够连接两个数组,同样不会改变原来的数组。用一个空数组连接另一个数组,即可实现深拷贝)
let arr3 = ['cat', 'dog', 'pig']
let arr4 = [].concat(arr3)
arr3[2] = 'big pig'
console.log(arr3) // ['cat', 'dog', 'big pig']
console.log(arr4) // ['cat', 'dog', 'pig']
ES6 ...拓展运算符
let arr5 = [0, 0, 1]
let arr6 = [...arr5]
arr5[0] = 10000
console.log(arr5) // [10000, 0, 1]
console.log(arr6) // [0, 0, 1]
我经常在数组的深拷贝中用到。
Array.from
Array.from()方法能从一个类似数组(伪数组)或可迭代对象中返回一个新的数组实例。通过Array.from()方法能获取到一个数组的深拷贝
let arr7 = [1, 2, 3]
let arr8 = Array.from(arr7)
arr7[1] = 1000
console.log(arr7) // [1, 1000, 3]
console.log(arr8) // [1, 2, 3]
2、Object的深拷贝
Object.assign()
ES6的Object.assign() Object.assign(target, …sources)用于对象的合并,将源对象中的所有可枚举属性,复制到目标对象中,并返回合并后的目标对象。后来的源对象的属性值,将会覆盖它之前的对象的属性。
let person = {
name: 'xia',
age: 25,
height: 160
}
let otherPerson = Object.assign({}, person)
person.age = 30
console.log(person)
console.log(otherPerson)
Object.create
Object.create 使用现有的对象来创建一个新的对象。同样的,也只能解决第一层的复制。
let person = {
name: "宁次",
age: 20,
sister: {
name: "雏田",
age: 18,
},
}
let person2 = Object.create(person)
person2.name = "鸣人"
alert(person.name) // 宁次, 第一层 ok
person2.sister.name = "鸣妹"
alert(person.sister.name) // 鸣妹,第二层仍是浅拷贝
注意:Object.create 对于数组的深拷贝不理想。
JSON.parse 和 JSON.stringify
先用 JSON.stringify 将对象序列化成字符串,再用 JSON.parse 解析回来,可以达到深拷贝的效果,并且对于多层对象同样有效
let person = {
name: "宁次",
age: 20,
sister: {
name: "雏田",
age: 18,
},
}
let person2 = JSON.parse(JSON.stringify(person))
person2.name = "鸣人"
alert(person.name) // 宁次, 第一层 ok
person2.sister.name = "鸣妹"
alert(person.sister.name) // 雏田,多层对象依旧 OK
注意:JSON.parse 和 JSON.stringify 依旧不是完美的,像一些方法或用户自定义的类型则不能有效的拷贝。
let person = {
name: "宁次",
age: 20,
sayHello() {
alert("你好~")
},
}
let person2 = JSON.parse(JSON.stringify(person))
person2.sayHello() // error:person2.sayHello is not a function
structuredClone
structuredClone 是内置的一个方法,对于多层对象也能很好地执行拷贝,但同样对方法或自定义对象无力。
let person = {
name: "宁次",
age: 20,
sister: {
name: "雏田",
age: 18,
},
}
let person2 = structuredClone(person)
person2.name = "鸣人"
alert(person.name) // 宁次, 第一层 ok
person2.sister.name = "鸣妹"
alert(person.sister.name) // 雏田,多层对象依旧 OK
对于方法或自定义对象无力无能为力。
let person = {
name: "宁次",
age: 20,
sayHello() {
alert("你好~")
},
}
// Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': sayHello() {alert("你好~")} could not be cloned
let person2 = structuredClone(person)
自己实现
自己实现的灵活度就很高,根据代码实现不同达到的效果与运行效率也会有所不同。
以下是三眼鸭通过递归实现的一个深拷贝函数,经测试能正确拷贝常见的多层对象、数组、方法。
// 深拷贝
function deepclone(obj) {
function copyList(arr) {
let result = []
for (let item of arr) {
result.push(this.deepclone(item))
}
return result
}
if (typeof obj === "object") {
if (Array.isArray(obj)) {
return copyList(obj)
} else {
let result = {}
for (let key in obj) {
result[key] = deepclone(obj[key])
}
return result
}
} else {
return obj
}
}
let person = {
name: "宁次",
age: 20,
sayHello() {
alert("你好")
},
sister: {
name: "雏田",
age: 18,
},
arr: [1, 2, 3],
}
let person2 = deepclone(person)
person2.name = "鸣人"
alert(person.name) // 宁次, 第一层 ok
person2.sister.name = "鸣妹"
alert(person.sister.name) // 雏田,多层对象依旧 OK
person2.arr[0] = "x"
alert(person.arr[0]) // 1, 数组也 ok
person2.sayHello() // 你好,方法也 ok
lodash
let person = {
name: "宁次",
age: 20,
sayHello() {
alert("你好")
},
sister: {
name: "雏田",
age: 18,
},
arr: [1, 2, 3],
}
let person2 = _.cloneDeep(person)
person2.name = "鸣人"
alert(person.name) // 宁次, 第一层 ok
person2.sister.name = "鸣妹"
alert(person.sister.name) // 雏田,多层对象依旧 OK
person2.arr[0] = "x"
alert(person.arr[0]) // 1, 数组也 ok
person2.sayHello() // 你好,方法也 ok
六、栈和堆的溢出六: 堆和栈的溢出
如果想要堆溢出,比较简单,可以循环创建对象或大的对象; 如果想要栈溢出,可以递归调用方法,这样随着栈深度的增加,JVM
(虚拟机)维持着一条长长的方法调用轨迹,直到内存不够分配,产生栈溢出。
总结:使用浅拷贝和深拷贝要取决于当时的场景
使用浅拷贝的情况下要留意对数据的修改,要想清楚修改后影响到所有引用的对象这个结果是否是你想要的,不是的话请使用深拷贝。
对于函数内部的代码则要尽量避免修改传入进来的对象,如果必须要修改,则要在函数说明中醒目提示。
也不要盲目频繁地使用深拷贝,以免造成大量的内存浪费。
对于深拷贝也不必选择大而全地完全拷贝的方法,如果只是单层对象或数组,简单地使用解构赋值就是最好的方法