快速搞定JS中的深浅拷贝

410 阅读5分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

前言

深浅拷贝是编程中非常重要的知识,在JS中,分为基本数据类型和引用数据类型,引用数据类型在进行赋值操作时传递的是指针的值,这就使得赋值后的变量只是原来变量的别名,开发中有时疏忽了这个特性,再加上JS是动态类型语言,因此debug也要花上不少时间。

浅拷贝的原理和实现

对于浅拷贝,我们可以理解为:

自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。

目前js实现浅拷贝的方式有很多,这里列举了几个常用的。

object.assign

assignES6Object的一个新增方法,其用于将一个对象中的所有可枚举属性复制到另一个对象中,然后返回复制后的那个对象。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
console.log(returnedTarget);
console.log(target === returnedTarget); 

> Object { a: 1, b: 4, c: 5 } 
> Object { a: 1, b: 4, c: 5 } 
> true
  • 在复制过程中,如果有相同的属性名,那么会进行覆盖操作,新的属性值将覆盖旧的属性值。
  • Object.assign只会拷贝对象的可枚举属性,包括Symbol,但是不会拷贝对象的继承属性。

concat、slice数组浅拷贝

对于数组对象的浅拷贝,可以使用concatslice方法。concat用于数组拼接,slice用于数组分片,它们都不会改变原始数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。会按以下规则进行拷贝:

  • 如果元素是个引用数据类型,那么会直接拷贝它的引用值到新数组。
  • 如果是基本数据类型,那么就直接拷贝这些值。
// concat
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);  // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]

//slice
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  //[ 1, 2, { val: 1000 } ]

对象扩展运算符

对象的扩展运算符是ES7中对ES6扩展运算符的补充,它通过调用对象内部保存键值的数组的迭代器,来获得对象的键值对。

/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} 
console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} 
console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果

通过扩展运算符复制的对象也是浅拷贝,它的缺点与Object.assign一样,但是对于属性都是基本类型的对象,使用扩展运算符更加的方便。

深拷贝的原理和实现

浅拷贝只是复制了最外层的对象,深拷贝需要逐层进行复制,遇到是对象,就分配新的内存。

将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。

要想逐层进行对象复制,那么我们就要使用递归处理,通过还要注意一些边界:

  • 除了要拷贝可枚举的值,还要拷贝不可枚举值、Symbol,以及对象原型链上的属性值。
  • 对于一些内置对象,例如DateRegExp这些值也要进行深拷贝。
  • 递归处理需要特别注意对象的循环引用。

代码如下,针对上面的边界值请看代码注释

// 使用weakMap来存储对象,判断循环引用,同时能防止内存泄漏
function deepClone(target,map = new WeakMap()){
  // 判断循环引用,如果map中已经存在当前对象,则直接返回
  if (map.has(target)) {
    return target;
  }
  // 判断Date,RegExp对象,直接返回一个新的实例
  if(target instanceof Date) return new Date(target)
  else if(target instanceof RegExp) return new RegExp(target)

  // 获得目标对象的属性描述对象
  const allDesc = Object.getOwnPropertyDescriptors(target)
  // 以目标对象的原型链,属性描述为基础创建一个新的对象,这样就解决了深克隆复制原型链和属性特性的问题
  const obj = Object.create(Object.getPrototypeOf(target),allDesc)
  map.set(target, obj)
  // 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法
  for (let key of Reflect.ownKeys(obj)) {
    if(typeof target[key] === 'object' && target[key] !== null){
      obj[key] = deepClone(target[key],map)
    }else{
      obj[key] = target[key]
    }
  }
  return obj
}

以上深拷贝实现只实现了部分功能,对于数组及ES6新增集合类型的深克隆需要编写额外的克隆代码。

总结

上面是大致说下了深浅克隆的原理和思路,最后的实现也不是最完整的,对ES6新增的集合类型复制都不支持,日常开发中,我们往往依靠一些工具库进行实现,如lodash_.cloneDeep以及_.clone