3种方法实现JS对象深拷贝

239 阅读3分钟

相信大家总是在各大博客中看到手写深拷贝这类题目,今天就分享3种方法实现它。

什么是深拷贝?

let a = { name: 'jiaqi', age: 100 };
let b = a;

我们知道对象是引用类型,它的值是地址,这个地址指向了堆中真正的数据。

如果直接将对象a赋值给b(b=a),此时a和b就会引用同样的数据。如果b修改某个属性,则也会修改a中属性。

b.name = '嘉琪';
console.log(a.name); //嘉琪

因此,如果想要实现两个对象互不影响(深拷贝),就得逐一复制它们的每一个属性。

1.使用JSON函数的方法

function deepClone(target) {
  return JSON.parse(JSON.stringify(target))
}

优点:

简单

实现了深拷贝

缺点:

不能克隆函数类型的属性,函数会被忽略掉

循环引用会报错

const str = { a: 1, b: [2], c: { t: [23] }, m() { return 2 } };

//循环引用
str.b.push(str.c);
str.c.j = str.b;

const test = deepClone(str);
console.log(test);

具体就是str.b = [2,str.c],而 str.c = {t:[23],j: str.b},也就是当拷贝到str.b中的第二个数据发现是一个对象,然后进入到这个对象进行拷贝,拷贝到t:[23],都是没问题的,但到了j。发现又是str.b,这样就会递归无尽了。

2.递归实现深拷贝

注解:

typeof null 返回的结果是 object,而众所周知 null 是一个基本类型,不是对象,前端培训所以我们判断是否为对象需要将它排除。

这里我们没有使用 for…in 操作符来遍历对象,因为in操作符会查找原型链上的属性,使用 Object.keys()更能节约性能。

我们提前判断了是否是对象或者数组,并创建了result容器。在之后,无论是数组还是对象,result[key] = value都能进行赋值。

function deepClone(target) {
  // 如果是对象,且不是原始值null
  if (typeof target === 'object' && target !== 'null') { //注解一
    // 创建容器
    const result = Array.isArray(target) ? [] : {}; //注解三
    const keys = Object.keys(target);  //注解二
	// Object.keys()会过滤掉原型链上的属性
    keys.forEach(key => {
      result[key] = deepClone(target[key])  // 注解三
    })
    return result;
  }
  // 如果是原始值,则直接返回
  return target;
}

优点:

可以克隆函数类型的属性

const str = { a: 1, b: [2], c: { t: [23] },m(){return 'hello'} };
const test = deepClone(str);
str.c.t = [3322332];
//实现了深拷贝
console.log(test.c.t);[23]
// 函数拷贝没问题
console.log(test.m()); //hello

缺点:

循环引用会报错

3.缓存克隆结果

如何解决循环引用会报错?

我们可以将每一步克隆的结果存储起来,如果之后再拷贝同样的内容,则直接返回已经克隆的内容。

在这里,使用 Map 这种数据类型:Map是一个带键的数据项的集合,就像一个 Object 一样。 但是它们最大的差别是 Map 允许任何类型的键(key)。

它的方法和属性如下:

new Map() —— 创建 map。
map.set(key, value) —— 根据键存储值。
map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。
map.has(key) —— 如果 key 存在则返回 true,否则返回 false。
map.delete(key) —— 删除指定键的值。
map.clear() —— 清空 map。
map.size —— 返回当前元素个数
function deepClone(target,map = new Map()) {
  // 如果是对象,且不是原始值null
  if (typeof target === 'object' && target !== 'null') {
    // 克隆前判断数据之前是否克隆过
    const cache = map.get(target);
    if (cache) {
      // 如果克隆过了,则直接返回
      return cache;
    }
    // 创建容器
    const result = Array.isArray(target) ? [] : {};
    
    // target为要被克隆的数据,result为克隆的结果
    // 把target做为键,result作为值
    // Map的好处在于键可以为任意类型
    // 等下如果又克隆到了相同的target,就直接从Map中读取数据
    map.set(target, result);
    
    const keys = Object.keys(target);
    keys.forEach(key => {
      // 我们需要把map传到函数deepclone中保留map的数据
      result[key] = deepClone(target[key],map)  //**** 真是机智啊
    })

    
    return result;
  }
  // 如果是原始值,则直接返回
  return target;
}

优点: 解决了循环引用。

这个方法另外令我最吃惊的地方在于保存 map 中的数据,因为这个数据是整个递归过程中都需要的。我呢,只能想到用闭包,而它是每一次递归都把数据直接传递给了函数。