前言
我们在
【JavaScript 数据结构】- 数组 / 栈 / 队列 / 链表
中已经知道了什么是堆,什么是栈。
简单来说就是:
-
栈:后进先出
-
堆:树状结构(二叉树),动态分配内存,内存大小不一,也不会自动释放。
栈内存和堆内存
我们知道, JavaScript 中的变量类型有:
-
基本值类型
JavaScript 中的 Boolean、Null、Undefined、Number、String、Symbol 都是基本类型。
保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问,并由系统自动分配和自动释放,这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。
-
引用类型
对象、数组、函数,所以 JavaScript 中的 Object、Array、Function、RegExp、Date 是引用类型。
是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。
举个例子来说明:
let a1 = 0; // 栈内存
let a2 = "this is string" // 栈内存
let a3 = null; // 栈内存
let b = { x: 10 }; // 变量 b 存在于栈中,{ x: 10 } 作为对象存在于堆中
let c = [1, 2, 3]; // 变量 c 存在于栈中,[1, 2, 3] 作为对象存在于堆中
所以当我们要访问堆内存中的引用数据类型时,实际操作为:
- 从栈中获取该对象的地址引用
- 再从堆内存中取得我们需要的数据
基本值类型和引用类型发生复制
基本类型发生复制 —— 在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值,最后这些变量都是 相互独立,互不影响的。
引用类型发生复制 —— 引用类型的复制,同样为新的变量 b 分配一个新的值,保存在栈内存中,不同的是,这个值仅仅是引用类型的一个地址指针。他们两个指向同一个值,也就是地址指针相同,在堆内存中访问到的具体对象实际上是同一个。 所以改变 b.x 时,a.x 也发生了变化,这就是引用类型的特性。
总结
| 栈内存 | 堆内存 |
|---|---|
| 存储基础数据类型 | 存储引用数据类型 |
| 按值访问 | 按引用访问 |
| 存储的值大小固定 | 存储的值大小不定,可动态调整 |
| 由系统自动分配内存空间 | 由代码进行指定分配 |
| 空间小,运行效率高 | 空间大,运行效率相对较低 |
| 先进后出,后进先出 | 无序存储,可根据引用直接获取 |
浅拷贝和深拷贝
我们上面说到的 引用类型的复制,其实就是 浅拷贝,复制得到的访问地址都指向同一个内存空间。所以修改了其中一个的值,另外一个也跟着改变了。
而 深拷贝 是什么呢?复制得到的访问地址指向不同的内存空间,互不相干。所以修改其中一个值,另外一个不会改变。即重新分配了一个内存空间,指针也是新的。
那么,如何进行深拷贝呢?
有个万金油方法:
let a = { x: 10, y: 20 }
let b = JSON.parse(JSON.stringify(a));
这样,通过 JSON.parse(JSON.stringify()) 方法转换过的,就是一个全新的地址,很简单的就完成了深拷贝。
但是呢,由于该方法数据量比较大时,会有性能问题,所以我们当然不能如此简单的就满足,我们可以来深入探讨一下,如何实现一个深拷贝方法。
由于 深拷贝一般发生在引用类型的复制上,引用类型我们可以大概分为数组和对象。
数组的深拷贝
数组进行深拷贝其实用很多原生的方法就能实现:
- 定义一个空数组,forEach 原数组,将 key 一个个 push 到空数组中。
- slice(),新数组 = 原数组.slice(0);
- concat(), 新数组 = 原数组.concat();
- ES6扩展运算符,[...新数组] = 原数组;
- JSON.parse(JSON.stringify())
对象的深拷贝
对象的深拷贝,实质上就是递归方法实现深度克隆:遍历对象、数组直到里边都是基本数据类型,然后再去复制。
在这里有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
function deepClone(obj, hash = new WeakMap()) {
// 如果是null或者undefined我就不进行拷贝操作
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== 'object') return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
let cloneObj = new obj.constructor();
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);