浅拷贝的原理和实现
对于浅拷贝的定义我们可以初步理解为:
自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。
实现浅拷贝的方法
object.assign
object.assign 是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
语法:object.assign 的语法为:Object.assign(target, …sources)
var obj = { a: 1 };
var obj1 = { b: { c: 1 } };
var obj2 = { d: 5 };
console.log(Object.assign(obj, obj1)); //{a:1,b:{c:1}}
console.log(Object.assign(obj, obj1, obj2)); // {a:1,b:{c:1},d:5}
var obj = {};
var obj1 = { a: 1, b: { c: 1 } };
Object.assign(obj, obj1);
console.log(obj); //{b:{c:10}}
obj1.a = 2;
console.log(obj); //{ a: 1, b: { c: 1 } }
console.log(obj1); //{ a: 2, b: { c: 1 } }
var obj = {};
var obj1 = { a: 1, b: { c: 1 } };
Object.assign(obj, obj1);
console.log(obj); //{b:{c:10}}
obj1.b.c = 10;
console.log(obj); //{a:1,b:{c:10}}
console.log(obj1); //{a:1,b:{c:10}}
使用Object.assign的注意点
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝 Symbol 类型的属性。
let obj1 = { a:{ b:1 }, sym:Symbol(1)};
// 添加一个不可枚举属性
Object.defineProperty(obj1, 'innumerable' ,{
value:'不可枚举属性',
enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1);//{ a:{ b:2 }, sym:Symbol(1),innumerable:'不可枚举属性'}
console.log('obj2',obj2);//{ a:{ b:2 }, sym:Symbol(1)}
扩展运算符方式
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj) // {a:1,b:{c:1}}
console.log(obj2) // {a:2,b:{c:1}}
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.b.c = 3
console.log(obj) // {a:1,b:{c:3}}
console.log(obj2) // {a:1,b:{c:3}}
concat拷贝数组
let arr = [1,2,3,[4]]
let newArr = arr.concat()
newArr[0] = 100
console.log(arr) // [1,2,3,[4]]
console.log(newArr) // [100,2,3,[4]]
let arr = [1,2,3,[4]]
let newArr = arr.concat()
newArr[3][0] = 50
console.log(arr) // [1,2,3,[50]]
console.log(newArr) // [1,2,3,[50]]
slice拷贝数组
let arr = [1,2,{val:4}]
let newArr = arr.slice()
arr[0] = 50
console.log(arr) // [50,2,{val:4}]
console.log(newArr) //[1,2,{val:4}]
let arr = [1,2,{val:4}]
let newArr = arr.slice()
arr[2].val = 100
console.log(arr) // [1,2,{val:100}]
console.log(newArr) // [1,2,{val:100}]
总结:从上面的代码中可以看出,这就是浅拷贝的限制所在了——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。
深拷贝的原理和实现
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。
这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下:
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
深拷贝的方法
乞丐版(JSON.stringify)
JSON.stringify() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。
let arr = [1, 2, [3, 4, 5]];
let arr1 = JSON.parse(JSON.stringify(arr));
arr[2].push(6);
console.log(arr); // [1, 2, [3, 4, 5, 6]]
console.log(arr1); // [1, 2, [3, 4, 5]]
从上面的代码可以看到,通过 JSON.stringify 可以初步实现一个数组的深拷贝,通过改变 arr 的 arr[2],其实可以看出 arr1 这个数组也不受影响。
使用JSON.stringify值得注意的点
- 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
- 拷贝 Date 引用类型会变成字符串;
- 无法拷贝不可枚举的属性;
- 无法拷贝对象的原型链;
- 拷贝 RegExp 引用类型会变成空对象;
- 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
- 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
let obj = {
a: undefined,
b: function () {},
c: Symbol(1),
d: new Date(),
e: /runoob/i,
f: NaN,
g: Infinity,
h: {
i: "嘿嘿嘿",
},
};
Object.defineProperty(obj, "innumerable", {
value: "不可枚举属性",
enumerable: false,
});
console.log()
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj1);
使用 JSON.stringify 方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify 暂时还是无法满足的.
基础版(手写递归)
let obj1 = {
a:{
b:1
}
}
function deepClone(obj) {
let cloneObj = {}
for(let key in obj) { //遍历
if(typeof obj[key] ==='object') {
cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key] //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); // {a:{b:1}}
console.log(obj1); //{a:{b:2}}
缺点
- 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
- 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
- 对象的属性里面成环,即循环引用没有解决。
改进版(改进后递归实现)
- 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
- 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
- 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;
- 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。
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) //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决
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)) {
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 // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)