cloneDeep 实现深拷贝

3,621 阅读3分钟

1. 背景知识

1.1 JavaScript中的数据类型

  • 基础类型分类

    • undefined
    • null
    • string
    • boolean
    • number
    • symbol (es6引入)
  • 引用类型分类

    • Object
    • Array
    • Function

1.2 堆 & 栈之分

  • 基础数据:存放在栈内存中,因其数据大小确定,内存空间大小可分配,按值存放;
  • 引用数据:存放在堆内存中,因其数据大小未知,内存空间大小不确定,按指针地址存放栈内存中,该指向堆内存中的地址存放的实际数据。

2. cloneShallow 浅拷贝 对比 cloneDeep 深拷贝

2.1 cloneShallow 浅拷贝

简单类型的数据,实际上不存在深浅拷贝之分,它们都存放在栈内存中,可直接使用,且是不可变数据,再进行拷贝时,是把值赋给新的变量,新变量值的改变并不会因影响旧变量值

let name = "Tom";
let name2 = name;
name2 = "Jerry"
console.log(name, name === name2);
// Tom  false

引用类型的数据,则无法通过=进行赋值拷贝,若按=则得到的只是该变量存放在栈内存中的引用指针,指向在堆内存中的数据一样

let obj = { name: "Tom" };
let obj1 = obj;
obj1.name = "Jerry";
console.log( obj, obj === obj1 );
// { name: "Jerry", true}

LOOK,此时对新对象obj1name进行修改,原对象objname也发生了改变,且两者在进行 === 运算时,返回true,可见两者实行的=赋值预算时,只是把引用指针的地址简单赋值,实现的只是浅拷贝

下面来看下如果实现深度拷贝

2.1 cloneDeep 深拷贝

(1)JSON API
let tom = { name: "tom", [Symbol()]: "symbol", say() {}, a: null, b: undefined };
let tom2 = JSON.parse(JSON.stringify(tom));
console.log(tom2);
// { name: "tom", a: null }

LOOK,上面的方法并不安全,如下罗列

该方法弊端:

  • 属性名为 Symbol值,会默认被忽略
  • 属性值为 null, function 时也会默认忽略掉

综上,该方法不建议使用

(2)手写cloneDeep函数
a. 首先代码展示:
function cloneDeep (obj, hash = new WeakMap()) {
  // 类型校验
  //  if (typeof obj == "undefined" ) return obj;
  // 校验null/ undefined不能用上述方法
  if (obj == null ) return obj;
  if (typeof obj !== "object") return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);

  // []/ {}   cloneObj
  let val = hash.get(obj);  
  if (val) { // 映射表 存在  直接将结果返回  避免重复地址
    return val; // 递归的终止条件
  }

  // 获取传入对象/方法的构造函数
  let cloneObj = new obj.constructor;
  
  for (let key in obj) {
    // 添加对象/ 数组 的自由属性,继承属性过滤
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = cloneDeep(obj[key], hash);
      hash.set(obj, cloneObj);
    }
  }
  return cloneObj;
}

let null1 = null;
let null2 = cloneDeep(null);
// console.log(null1 === null2);   // true

let date = new Date();
let date2 = cloneDeep(date);
console.log(date === date2);   // false

let obj = { a: 1};
let obj2 = cloneDeep(obj);
obj2.a = 2;
console.log(obj.a);   // 1

let arr = [1, 2, 3, 4];
let arr1 = cloneDeep(arr);
arr1.unshift(0);
console.log(`arr: ${arr}; arr1 ${arr1}`);
// arr: 1,2,3,4; arr1 0,1,2,3,4

let cobj = { a: 1 };
cobj.b = cobj;
let cobj1 = cloneDeep(cobj);
cobj1.a = 2;
console.log(cobj, cobj1);

考察点:类型校验 + hash表

b. 代码解析步骤

先要清楚,哪些属性是不需要进行校验直接返回的:

  • 简单类型的值
  • 函数

首先,类型校验:

  • typeof

    • null 和 undefined
    • function
  • instanceof

    • Date
    • RegExp
  • constructor

    • Object
    • Array
    • Map
    • WeakMap
    • Set
    • WeakSet
    • ...

对象属性如果存在循环引用:

let obj = { a: 1, b: 2 };
obj.c = obj;

通过hash表进行解决,同时在循环递归时,运用WeakMap内部属性不可重复,类似(WeakSet特性),可作为判断递归终止条件,同时避免造成内存泄露