这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战
前言
深浅拷贝是编程中非常重要的知识,在JS
中,分为基本数据类型和引用数据类型,引用数据类型在进行赋值操作时传递的是指针的值,这就使得赋值后的变量只是原来变量的别名,开发中有时疏忽了这个特性,再加上JS
是动态类型语言,因此debug
也要花上不少时间。
浅拷贝的原理和实现
对于浅拷贝,我们可以理解为:
自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。
目前js实现浅拷贝的方式有很多,这里列举了几个常用的。
object.assign
assign
是ES6
中Object
的一个新增方法,其用于将一个对象中的所有可枚举属性复制到另一个对象中,然后返回复制后的那个对象。
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数组浅拷贝
对于数组对象的浅拷贝,可以使用concat
、slice
方法。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
,以及对象原型链上的属性值。 - 对于一些内置对象,例如
Date
、RegExp
这些值也要进行深拷贝。 - 递归处理需要特别注意对象的循环引用。
代码如下,针对上面的边界值请看代码注释
// 使用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
。