【数据结构】- 栈内存和堆内存,浅拷贝和深拷贝

518 阅读5分钟

前言

我们在

【JavaScript 数据结构】- 数组 / 栈 / 队列 / 链表

【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] 作为对象存在于堆中

所以当我们要访问堆内存中的引用数据类型时,实际操作为:

  1. 从栈中获取该对象的地址引用
  2. 再从堆内存中取得我们需要的数据

基本值类型和引用类型发生复制

基本类型发生复制 —— 在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值,最后这些变量都是 相互独立,互不影响的。

引用类型发生复制 —— 引用类型的复制,同样为新的变量 b 分配一个新的值,保存在栈内存中,不同的是,这个值仅仅是引用类型的一个地址指针。他们两个指向同一个值,也就是地址指针相同,在堆内存中访问到的具体对象实际上是同一个。 所以改变 b.x 时,a.x 也发生了变化,这就是引用类型的特性。

总结

栈内存堆内存
存储基础数据类型存储引用数据类型
按值访问按引用访问
存储的值大小固定存储的值大小不定,可动态调整
由系统自动分配内存空间由代码进行指定分配
空间小,运行效率高空间大,运行效率相对较低
先进后出,后进先出无序存储,可根据引用直接获取

浅拷贝和深拷贝

我们上面说到的 引用类型的复制,其实就是 浅拷贝,复制得到的访问地址都指向同一个内存空间。所以修改了其中一个的值,另外一个也跟着改变了。

深拷贝 是什么呢?复制得到的访问地址指向不同的内存空间,互不相干。所以修改其中一个值,另外一个不会改变。即重新分配了一个内存空间,指针也是新的。

那么,如何进行深拷贝呢?

有个万金油方法:

let a = { x: 10, y: 20 }
let b = JSON.parse(JSON.stringify(a));

这样,通过 JSON.parse(JSON.stringify()) 方法转换过的,就是一个全新的地址,很简单的就完成了深拷贝。

但是呢,由于该方法数据量比较大时,会有性能问题,所以我们当然不能如此简单的就满足,我们可以来深入探讨一下,如何实现一个深拷贝方法。

由于 深拷贝一般发生在引用类型的复制上,引用类型我们可以大概分为数组和对象。

数组的深拷贝

数组进行深拷贝其实用很多原生的方法就能实现:

  1. 定义一个空数组,forEach 原数组,将 key 一个个 push 到空数组中。
  2. slice(),新数组 = 原数组.slice(0);
  3. concat(), 新数组 = 原数组.concat();
  4. ES6扩展运算符,[...新数组] = 原数组;
  5. 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);