深拷贝和浅拷贝以及了解数据类型

147 阅读7分钟

JavaScript 中的变量分为基本类型和引用类型。

  • 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问,并由系统自动分配和自动释放。 这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。 JavaScript 中的 String、Number、Boolean、Null、Undefined、Symbol、bigInt 都是基本类型。

  • 引用类型(如对象、数组、函数等)是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。 JavaScript 中的 Object、Array、Function、RegExp、Date 是引用类型。

我理解,是因为基本类型因为占用空间小,大小也固定,随意保存在栈内存中。而引用数据类型占据空间大、大小不固定,所以存放在堆内容中,如果存储在栈中,将影响程序的运行性能。

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

image.png

当我们要访问堆内存中的引用数据类型时

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

基本类型发生复制

let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5

image.png

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

引用类型发生复制

let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5
  • 引用类型的复制,同样为新的变量 b 分配一个新的值,保存在栈内存中,不同的是,这个值仅仅是引用类型的一个地址指针。

  • 他们两个指向同一个值,也就是地址指针相同,在堆内存中访问到的具体对象实际上是同一个。

  • 因此改变 b.x 时,a.x 也发生了变化,这就是引用类型的特性。

image.png

var obj = {name:'ConardLi'};
var obj2 = obj;
obj2.name = 'code秘密花园';
console.log(obj.name); // code秘密花园

image.png 我的理解:对象obj1有一个代号(OXOOO1)存放在栈内存中,他的实际内容是在堆内存中开辟了一个空间进行存储,当拷贝引用类型obj1时,如果使用等号或者一些浅拷贝方法,比如obj2=obj1,那么实际上是在栈内存中,多了一个obj2代号(OXOOO2),但是他们俩在堆内存中都指向同一个内存空间。所以呢此时不管是操作obj1还是obj2,其实操作的都是同一个数据。

以数组为例,它的很多方法都可以改变它自身。

  • pop() 删除数组最后一个元素,如果数组为空,则不改变数组,返回undefined,改变原数组,返回被删除的元素

  • push()向数组末尾添加一个或多个元素,改变原数组,返回新数组的长度

  • shift()把数组的第一个元素删除,若空数组,不进行任何操作,返回undefined,改变原数组,返回第一个元素的值

  • unshift()向数组的开头添加一个或多个元素,改变原数组,返回新数组的长度

  • reverse()颠倒数组中元素的顺序,改变原数组,返回该数组

  • sort()对数组元素进行排序,改变原数组,返回该数组

  • splice()从数组中添加/删除项目,改变原数组,返回被删除的元素

浅拷贝与深拷贝

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以引用类型中,如果其中一个对象改变了这个地址,就会影响到另一个对象

  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。复制得到的访问地址指向不同的内存空间,互不相干

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

平时使用数组复制时,我们大多数会使用 =,这只是浅拷贝,存在很多问题。浅拷贝下,拷贝和被拷贝的数组会相互受到影响。所以,必须要有一种不受影响的方法,那就是深拷贝。

无论是 concat, Spread syntax 还是 Object.assign 执行的都是浅拷贝,不是深拷贝,也就是只遍历一层。

concat本质上也是浅拷贝,只能实现一维对象的深拷贝。 只能实现数组中是简单类型的,如果数组中有复杂类型,对象这种,就出事了。

let arr_1 = [1, 2, false, 'a']
let arr_2 = [].concat(arr_1)

arr_1[1] = 3

// arr_1中的数据更改,并不会影响arr_2
console.log(arr_1) // -> [1, 3, false, 'a']
console.log(arr_2) // -> [1, 2, false, 'a']

如果数组中有复杂数据类型,它就出事儿了

let arr_1 = [1, 2, false, {a: 1}]
let arr_2 = [].concat(arr_1)

arr_1[3].a = 2

// arr_1中的数据更改,arr_2中的数据会跟着变
console.log(arr_1) // -> [1, 2, false, {a: 2}]
console.log(arr_2) // -> [1, 2, false, {a: 2}]

数组和对象的拷贝方法:

浅拷贝数组

  • Array.from()
var arr1=[1,2,3];
var arr2=Array.from(arr1);
  • 扩展运算符...
var arr1=[1,2,3];
var arr2=[...arr1];

浅拷贝对象

  • Object.assign()
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' }; 
let obj2 = Object.assign({}, obj1);
  • 展开运算符...
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

深拷贝

  • JSON.parse(JSON.stringify(obj)) 不能用于处理函数和正则
let a = { x: 10, y: 20 }
let b = JSON.parse(JSON.stringify(a));
b.x = 5;
console.log(a.x); // 10
console.log(b.x); // 5

这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

  • 函数库lodash的_.cloneDeep方法
var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

Lodash的cloneDeep方法非常强大,能够处理各种情况。

  • 手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。关于这块如有疑惑,请仔细阅读ConardLi大佬如何写出一个惊艳面试官的深拷贝?这篇文章。

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  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);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 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);

建议阅读

juejin.cn/post/684490…

juejin.cn/post/684490…

juejin.cn/post/684490…

juejin.cn/post/684490…