深拷贝 & 浅拷贝

272 阅读5分钟

www.jianshu.com/p/c651aeabf… (这里有解决循环引用的方法)

juejin.im/post/5b00e8…

juejin.cn/post/687031…

juejin.cn/post/703132…

之所以会出现了深拷贝和浅拷贝,究其根本是因为JS种的变量包含了不同类型的数据值:基本类型和引用类型;同时其中变量的存储方式也不同,Object属于堆内存的储存方式;并且JS的值的传递方式如值传递和址传递的会出现不同,所以才会出现这一系列的问题,因此才会使用深拷贝和浅拷贝来解决这些问题。

不同类型的存储方式:

  • 基本类型:基本类型值在内存中占据固定大小,保存在栈内存中
  • 引用类型:引用类型的值是对象,保存在堆内存中,而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址

不同类型的复制方式:

  • 基本类型:从一个变量向另外一个新变量复制基本类型的值,会创建这个值的一个副本,并将该副本复制给新变量(传值)
let foo = 1;
let bar = foo;
console.log(foo === bar); // -> true // 修改foo变量的值并不会影响bar变量的值

let foo = 233;
console.log(foo); // -> 233
console.log(bar); // -> 1
  • 引用类型:从一个变量向另一个新变量复制引用类型的值,其实复制的是指针,最终两个变量最终都指向同一个对象(传址)
let foo = { name: "leeper", age: 20 };
let bar = foo;
console.log(foo === bar); // -> true

// 改变foo变量的值会影响bar变量的值
foo.age = 19;
console.log(foo); // -> {name: 'leeper', age: 19}
console.log(bar); // -> {name: 'leeper', age: 19}

深拷贝 & 浅拷贝

  • 浅拷贝:仅仅是复制了引用,彼此之间的操作会互相影响
  • 深拷贝:在堆中重新分配内存,不同的地址,相同的值,互不影响

总的来说,深浅拷贝的主要区别就是:复制的是引用还是复制的是实例

浅拷贝 
Array.prototype.slice() 
Array.prototype.concat() 
Object.assign() 
扩展运算符 var cloneObj = { ...obj }; 

深拷贝 
JSON.parse()和JSON.stringify() // 不支持值为undefined、函数和循环引用的情况 
lodash 
jQuery

原生JavaScript中提供的一些复制方法:

浅拷贝:

  1. 1.Array.prototype.slice()、Array.prototype.concat()
let a = [1, 2, 3, 4];
let b = a.slice();
console.log(a === b); // -> false
a[0] = 5;
console.log(a); // -> [5, 2, 3, 4]
console.log(b); // -> [1, 2, 3, 4]

let a = [[1, 2], 3, 4];
let b = a.slice();
console.log(a === b); // -> false
a[0][0] = 0;
console.log(a); // -> [[0, 2], 3, 4]
console.log(b); // -> [[0, 2], 3, 4]

Array的slice和concat方法并不是真正的深拷贝,对于Array的第一层的元素是深拷贝,而Array的第二层 slice和concat方法是复制引用。所以,Array的slice和concat方法都是浅拷贝。

  1. Object.assign()
  2. 扩展运算符 var cloneObj = { ...obj };
  3. ll

手动实现浅拷贝

var a = { name: "hello" };
var b = copy(a);
b.name = "hi";
console.log(a);
function copy(obj) {
  var result = {};
  for (var attr in obj) {
    result[attr] = obj[attr];
  }
  return result;
}

深拷贝:JSON.parse()和JSON.stringify()

let obj = { name: "leeper", age: 20, friend: { name: "lee", age: 19 } };
let copyObj = JSON.parse(JSON.stringify(obj));
obj.name = "Sandman";
obj.friend.name = "Jerry";
console.log(obj);
// -> {name: "Sandman", age: 20, friend: {age: 19,name: 'Jerry'}}
console.log(copyObj);
// -> {name: "leeper", age: 20, friend: {age: 19,name: 'lee'}}
  1. JSON.stringify():把一个js对象序列化为一个JSON字符串
  2. JSON.parse():把JSON字符串反序列化为一个js对象

// 不支持值为undefined、函数和循环引用的情况

const cloneObj = JSON.parse(JSON.stringify(obj))

2.lodash

3.jQuery

动手实现深拷贝

利用递归来实现对对象或数组的深拷贝。递归思路:对属性中所有引用类型的值进行遍历,直到是基本类型值为止。

function deepCopy(obj) {
  if (!obj && typeof obj !== "object") {
    throw new Error("error arguments");
  }
  const targetObj = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    //只对对象自有属性进行拷贝(规避原型链属性上的拷贝)
    if (obj.hasOwnProperty(key)) {
      if (obj[key] && typeof obj[key] === "object") {
        targetObj[key] = deepCopy(obj[key]);
      } else {
        targetObj[key] = obj[key];
      }
    }
  }
  return targetObj;
}

但是还有很多问题 首先这个deepClone函数并不能复制不可枚举的属性以及Symbol类型 这里只是针对Object引用类型的值做的循环迭代,而对于Array,Date,RegExp,Error, Function引用类型无法正确拷贝 对象循环引用成环了的情况

一个简单地深拷贝

var obj1 = { a: { b: 1 }, c: 1 };
var obj2 = {};
obj2.a = {};
obj2.c = obj1.c;
obj2.a.b = obj1.a.b;
console.log(obj1); //{a:{b:1},c:1};
console.log(obj2); //{a:{b:1},c:1};
obj1.a.b = 2;
console.log(obj1); //{a:{b:2},c:1};
console.log(obj2); //{a:{b:1},c:1};

在上面的代码中,我们新建了一个obj2对象,同时根据obj1对象的a属性是一个引用类型,我们给obj2.a的值也新建一个新对象(即在内存中新开辟了一块内存地址),然后把obj1.a.b属性的值数字1复制给obj2.a.b,因为数字1是基本类型的值,所以改变obj1.a.b的值后,obj2.a不会收到影响,因为他们的引用是完全2个独立的对象,这就完成了一个简单的深拷贝

可以解决上述问题得深拷贝

这个函数有几个要点

  1. 利用Reflect.ownKeys()方法,能够遍历对象的不可枚举属性和Symbol类型
  2. 当参数为Date,RegExp类型则直接生成一个新的实例
  3. 使用Object.getOwnPropertyDescriptors()获得对象的所有属性对应的特性,结合Object.create()创建一个新对象继承传入原对象的原型链
  4. 利用WeekMap()类型作为哈希表,WeekMap()因为是弱引用的可以有效的防止内存泄露,作为检测循环引用很有帮助,如果存在循环引用直接返回WeekMap()存储的值
const isComplexDataType = (obj) =>
  (typeof obj === "object" || typeof obj === "function") && obj !== null;
const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) return new Date(obj); //日期对象就返回一个新的日期对象
  if (obj.constructor === RegExp) return new RegExp(obj);
  //正则对象就返回一个新的正则对象
  //如果成环了,参数obj = obj.loop = 最初的obj会在WeakMap中找到
  //第一次放入的obj提前返回第一次放入WeakMap的cloneObj
  if (hash.has(obj)) return hash.get(obj);
  let allDesc = Object.getOwnPropertyDescriptors(obj); //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc); //继承原型链
  hash.set(obj, cloneObj);
  for (let key of Reflect.ownKeys(obj)) {
    //Reflect.ownKeys(obj)可以拷贝不可枚举属性和符号类型
    // 如果值是引用类型(非函数)则递归调用deepClone
    cloneObj[key] =
      isComplexDataType(obj[key]) && typeof obj[key] !== "function"
        ? deepClone(obj[key], hash)
        : obj[key];
  }
  return cloneObj;
};

let obj = {
  num: 0,
  str: "",
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: "我是一个对象", id: 1 },
  arr: [0, 1, 2],
  func: function () {
    console.log("我是一个函数");
  },
  date: new Date(0),
  reg: new RegExp("/我是一个正则/ig"),
  [Symbol("1")]: 1,
};
Object.defineProperty(obj, "innumerable", {
  enumerable: false,
  value: "不可枚举属性",
});
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj));
obj.loop = obj;
let cloneObj = deepClone(obj);
console.log("obj", obj);
console.log("cloneObj", cloneObj);
for (let key of Object.keys(cloneObj)) {
  if (
    typeof cloneObj[key] === "object" ||
    typeof cloneObj[key] === "function"
  ) {
    console.log(`${key}相同吗? `, cloneObj[key] === obj[key]);
  }
}