yanyue404/blog

Javascript之深浅拷贝

yanyue404 opened this issue · 0 comments

前言

深拷贝拷贝的是两个完全相同的对象,两个双胞胎长得一摸一样,互不影响。

浅拷贝拷贝的是指向对象的指针,两个指针同样指向同同一对象,一改都改变。

浅拷贝:浅拷贝是拷贝引用,拷贝后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响。

深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响

只是针对复杂数据类型(Object,Array)的复制问题。浅拷贝与深拷贝都可以实现在已有对象上再生出一份的作用。但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此拷贝的时候就存在两种情况了:拷贝引用和拷贝实例,这也是浅拷贝和深拷贝的区别。

浅拷贝

简单的浅拷贝可以使用数组的concatslice做到:

var arr = ["old", 1, true, null, undefined];
var new_arr = [].concat(arr);

new_arr[0] = "new";

console.log(arr); //['old',1 ,true, null, undefined]
console.log(new_arr); //['new',1, true, null, undefined]

查看第一个例子后可能以为concat是深拷贝了实例,下面接着看复杂一些的数组能不能做到:

var arr = [{ old: "old" }, ["old"]];

var new_arr = arr.concat();

arr[0].old = "new";
arr[1][0] = "new";

console.log(arr); // [{old: 'new'}, ['new']]
console.log(new_arr); // [{old: 'new'}, ['new']]

在这里看到concat对于复杂的例子是无法完成深拷贝的,更改实例 1 后实例 2 也进行了相同的变化,还有slice,它们完成的是浅拷贝。

外层源对象是拷贝实例,如果其属性元素为复杂数据类型时,内层元素拷贝引用。

常用方法:Array.prototype.slice(), Array.prototype.concat()Object.assign()解构赋值([...args], {..obj})

浅拷贝的实现

const shallowCopy = function (obj) {
  // 只拷贝对象
  if (typeof obj !== "object") return;
  var newObj = obj instanceof Array ? [] : {};
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key];
    }
  }
  return newObj;
};

深拷贝

深拷贝后,两个对象,包括其内部的元素互不干扰。常见方法有 JSON.parse(JSON.stringify(obj)),jQury 的 $.extend(true,{},obj),lodash 的cloneDeep(推荐使用)。

实现方式

  1. 用 JSON,存在如下缺点:
  • 不支持 Date、正则、undefined、函数等数据
  • 不支持引用(即环状结构)
const deepClone = (o) => JSON.parse(JSON.stringify(o));
  1. 简易版(新增函数函数类型支持):
function deepCopy(target) {
  if (typeof target == "object") {
    const result = Array.isArray(target) ? [] : {};
    for (const key in target) {
      if (typeof target[key] == "object") {
        result[key] = deepCopy(target[key]);
      } else {
        result[key] = target[key];
      }
    }
    return result;
  } else if (typeof target == "function") {
    return eval("(" + target.toString() + ")");
    // 也可以这样克隆函数
    // return new Function("return " + target.toString())();
  } else {
    return target;
  }
}
  1. 考虑循环引用

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储 key-value 形式的数据,且 key 可以是一个引用类型,我们可以选择 Map 这种数据结构:

  • 检查 map 中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为 key,克隆对象作为 value 进行存储
  • 继续克隆
function clone(target, map = new Map()) {
  if (typeof target === "object") {
    let cloneTarget = Array.isArray(target) ? [] : {};
    if (map.get(target)) {
      return map.get(target);
    }
    map.set(target, cloneTarget);
    for (const key in target) {
      cloneTarget[key] = clone(target[key], map);
    }
    return cloneTarget;
  } else {
    return target;
  }
}
  1. 递归完整版本

实现要点:

  • 递归
  • 判断类型
  • 新增对象类型 Function、Date、RegExp 支持
  • 检查环
const deepClone = (o, cache = new WeakMap()) => {
  if (o instanceof Object) {
    if (cache.get(o)) {
      return cache.get(o);
    }
    let result;

    if (o instanceof Function) {
      // 有 prototype 就是普通函数
      if (o.prototype) {
        result = function () {
          return o.apply(this, arguments);
        };
      } else {
        result = (...args) => {
          return o.call(undefined, ...args);
        };
      }
    } else if (o instanceof Array) {
      result = [];
    } else if (o instanceof Date) {
      return +new Date(o);
    } else if (o instanceof RegExp) {
      result = new RegExp(o.source, o.flags);
    } else {
      // 最后是普通对象
      result = {};
    }
    // ! 只要拷贝过下次就不要拷贝了
    cache.set(o, result);
    for (const key in o) {
      if (o.hasOwnProperty(key)) {
        result[key] = deepClone(o[key], cache);
      }
    }
    return result;
  } else {
    // string、number、boolean、undefined、null、symbol、bigint
    return o;
  }
};
const a = {
  number: 1,
  bool: false,
  str: "hi",
  empty1: undefined,
  empty2: null,
  array: [
    { name: "frank", age: 18 },
    { name: "jacky", age: 19 },
  ],
  date: new Date(2000, 0, 1, 20, 30, 0),
  regex: /\.(j|t)sx/i,
  obj: { name: "frank", age: 18 },
  f1: (a, b) => a + b,
  f2: function (a, b) {
    return a + b;
  },
};
a.self = a;
var b = deepClone(a);
console.log(b);
console.log(b.self === b);

补充:Why WeakMap

WeakMap 的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将 obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

参考