2022,手写一个js深拷贝压压惊

188 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第12天,点击查看活动详情

日常开发中,免不了面向"CV"编程。JavaScript中有两大数据类型:基本类型、引用类型,不同的数据类型之间复制数据的方式也不太一样。本文重点探讨引用类型的深拷贝。

深拷贝和浅拷贝的区别

深拷贝和浅拷贝最大的区别就在于如何处理引用数据类型。我们都知道,引用数据类型是存在堆中的,引用地址是存在栈中的。对于浅拷贝来说,会把引用数据类型的复制给目标对象,因此浅拷贝引用数据类型,将指向同一个对象。但是深拷贝是新创建一个对象,新创建的对象和源引用数据不是指向同一内存空间,因此深拷贝引用数据类型,不是同一个对象。

内存结构.png

如何实现深拷贝呢,

使用JSON.stringfy()和parse

使用JSON的序列化和反序列化可以实现简单引用数据类型的深拷贝,实现过程就是先JSON.stringify()将引用数据转换为字符串,然后使用JSON.parse()将JSON字符串解析为对象,在此过程中涉及新创建对象,因此深拷贝前后不是同一个对象。

刚才也说了,使用JSON序列化/反序列化只能适用于简单的引用数据类型,以下情况就不适用:

  • 源对象中存在函数
  • 源对象存在循环引用
  • 源对象中存在Map、Set数据类型

以上数据类型,在使用JSON.stringify()时,会忽略转换,因此解析时就会存在数据丢失。

举个栗子

let obj = {fn: () => {}, x:1};
JSON.stringify(obj); // "{"x": 1}",函数会被忽略
obj.z = new Set([1,2,3]);
JSON.stringify(obj); // "{"x": 1, "z": {}}", // 无法转换Set、Map,因为Set和Map的属性不可枚举
obj.y = obj;
JSON.stringify(obj); // 报错,无法转换循环引用

使用Object.assign()

Object.assign()用于将一个或多个源对象的所有可枚举属性复制到目标对象中,并且返回目标对象,注意此方法不是深拷贝。原因就在于它是复制对象的属性,并没有创建新的对象。

举个栗子

let a1 = {a: 1, b: 2};
let a2 = {b: 3, c: 4};
let a3 = Object.assign(a1, a2);
a1.a = 0;
console.log(a1, a3); // {a:0,b:3,c:4},{a:0,b:3,c:4},由此可见并不是深拷贝

深拷贝

手写深拷贝,需要考虑以下情况:

  • 处理Object、Array、Map、Set等数据类型
  • 处理循环引用
  • 处理函数

实现代码:

/**
 * 深拷贝
 * @param {*} obj 任意对象
 * @param {*} map 避免循环引用,使用WeakMap既保留了Map的get/set,同时也可以避免内存泄漏
 */
export const deepClone = function (obj, map = new WeakMap()) {
  // 判断null时采用非严格判断,包含null和undefined的情况
  if (obj == null || typeof obj !== 'object') {
    return obj; // 非引用类型或者是null、undefined,就直接返回
  }
​
  // 处理循环引用
  let objMap = map.get(obj);
  // 如果存在循环引用,则直接返回
  if (objMap) return objMap;
  let result = {};
  map.set(obj, result);
​
  // 处理Map类型
  // 细分类型的判断,使用instanceof
  if (obj instanceof Map) {
    result = new Map(); // 创建一个新的Map
    obj.forEach((v, k) => {
      let v1 = deepClone(v, map); // 递归调用
      let k1 = deepClone(k, map); // Map的key,可能是引用数据类型,因此需要递归调用
      result.set(k1, v1);
    })
  }
​
  // 处理Set类型
  if (obj instanceof Set) {
    result = new Set();
    obj.forEach(v => {
      let v1 = deepClone(v, map);
      result.add(v1);
    })
  }
​
  // 处理Array数组
  if (obj instanceof Array) {
    // 调用数组的map遍历并返回新的数组
    result = obj.map(item => deepClone(item, map));
  }
​
  // 处理Object
  for(let k in obj) {
    // Object的key是基本数据类型,不存在深拷贝
    let v1 = deepClone(obj[k], map);
    result[k] = v1;
  }
​
  return result;
}

测试一下代码效果:

// 测试
let obj = {
  set: new Set([1,2,3]),
  map: new Map([['x', 1], ['y', 2]]),
  object: {
    z: 1
  },
  arr: [1,2,3],
  fn: () => {
    console.log('function');
  }
}
obj.self = obj;
let res = deepClone(obj);
obj.object.z = 3;
console.log(res.object.z); // 3

总结

  • 深拷贝是新创建一个对象,原对象和目标对象在内存中是不同的值,彼此修改互补影响。
  • 手写实现深拷贝,需要考虑函数、循环引用、Map、Set等数据类型
  • 实现深拷贝,需要充分利用递归函数

原创不易,转载请注明出处