浅拷贝
概念
JS的数据类型分为原始类型和引用类型,当我们将一个引用类型的变量赋值给另一个变量时,往往会发生如下情况:
const obj1 = {name: 'wyt'};
const obj2 = obj1;
console.log(obj1.name); // 'wyt'
obj2.name = 'huluntuntao';
console.log(obj2.name); // 'huluntuntao'
console.log(obj1.name); // 'huluntuntao'
由于引用类型的原因,会导致obj2改变之后obj1也随之改变,但是这种现象在我们很多情况下是不想让它存在的,因此就有了深拷贝与浅拷贝。
浅拷贝会创建一个新的对象,当拷贝对象的属性值为
原始类型时,拷贝它的值,当拷贝对象的属性值为引用类型时,拷贝其引用地址。
简单点来说,就是浅拷贝可以拷贝对象属性的原始类型,但是拷贝对象属性的引用类型时,还是会产生上述情况。
也可以这么说,浅拷贝会创建一个新的地址存放想要拷贝的那个对象,但是对于对象内部其他引用类型的值,并不做处理。
接下去是几种常见的浅拷贝方法:
Object.assign()
Object.assign()算是在JS中比较常用到的浅拷贝方法:
const obj1 = {
name: 'wyt',
other: {
age: 25
}
};
const obj2 = Object.assign({}, obj1);
// 第一个参数是拷贝到目标对象,第二个参数是需要拷贝的对象;
console.log(obj1.name); // 'wyt'
obj2.name = 'huluntuntao';
console.log(obj2.name); // 'huluntuntao'
console.log(obj1.name); // 'wyt'
obj2.other.age = 30;
console.log(obj1.other.age) // 30
console.log(obj2.other.age) // 30
以上代码也可以很明显的看出来,浅拷贝只能拷贝对象属性为原始类型的值,对于对象属性值为引用类型时,还是不能完全解决问题。
使用Object.assign()时的注意点:
- 可以拷贝Symbol类型
- 不能拷贝对象的继承属性
- 不能拷贝不可枚举的属性
const obj1 = { a: { b: 1 }, sym: Symbol(1), [Symbol(2)]: 'hahah' };
Object.defineProperty(obj1, 'innumerable', {
value: '这是一个不可枚举属性',
enumerable: false
});
const obj2 = Object.assign({}, obj1);
console.log(obj2); // 结果如下图
扩展运算符(...)
扩展运算符的使用如下,基本与Object.assign()没有差别:
const obj1 = {
name: 'wyt',
other: {
age: 25
}
};
const obj2 = {...obj1};
console.log(obj1.name); // 'wyt'
obj2.name = 'huluntuntao';
console.log(obj2.name); // 'huluntuntao'
console.log(obj1.name); // 'wyt'
obj2.other.age = 30;
console.log(obj1.other.age) // 30
console.log(obj2.other.age) // 30
concat()和slice()
concat()和slice()只能对数组进行浅拷贝,都是会返回一个新的数组对象,并且不改变原数组。
自己实现一个浅拷贝
function MyAssign(source) {
if (typeof source !== 'object' || source === null) {
return source;
}
// 判断是数组还是对象,并根据判断创建初始值
const target = Array.isArray(source) ? [] : {};
// 循环遍历source
for (let key in source) {
// 判断当前属性是否为自身属性(hasOwnProperty()检测对象在排除原型链的情况下是否具有某个属性)
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}
什么是深拷贝
概念
浅拷贝在拷贝对象属性值为引用类型时,就只会拷贝属性值的引用地址。
深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
JSON.stringify()
JSON.stringify()是我们平时开发中比较常用的深拷贝方式,它通过先将对象序列化成string类型,再用JSON.parse()方式反序列化得到一个全新的对象,从而实现深拷贝。
const obj1 = {
name: 'wyt',
other: {
age: 25
}
};
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.other.age = 30;
console.log(obj2.other.age); // 30
console.log(obj1.other.age); // 25
JSON.stringify()虽然基本上能满足我们日常开发时深拷贝的需求,但是它其实存在许多问题:
- 无法拷贝不可枚举属性
- 对于正则对象(RegExp())拷贝后返回空对象({});
- 对于日期对象(Date())拷贝后返回字符串;
- 当属性值为undenfined、Symbol和函数时,其键值对会消失;
- 无法拷贝对象原型链上的属性;
- 拷贝循环引用的对象(即对象成环 (obj[key] = obj))时会报错;
- 对象中含有NaN,Infinity和-Infinity的值时,序列化结果会返回null;
function Obj() {
this.func = function () { alert(1) };
this.obj = {a:1};
this.arr = [1,2,3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable:false,
value:'innumerable'
});
console.log('obj1', obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);
const obj1 = {
name: 'wyt',
other: {
age: 30
}
};
obj1.other.age = obj1;
const obj2 = JSON.stringify(obj1);
简单版深拷贝实现
实现一下最基础的深拷贝:
function easyDeepClone(source) {
if (typeof source !== 'object' || source === null) {
return source;
}
// 判断是数组还是对象,并根据判断创建初始值
const target = Array.isArray(source) ? [] : {};
// 循环遍历source
for (let key in source) {
// 判断当前属性是否为引用类型
if (typeof source[key] === 'object') {
target[key] = easyDeepClone(source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
上述深拷贝函数还存在一些问题:
- 无法拷贝不可枚举属性;
- 这种方法只是针对普通的引用类型的值做递归复制,而对于 Date、RegExp、Function 这样的引用类型并不能正确地拷贝;
- 无法解决循环引用的问题;
进阶版深拷贝实现
// 利用一个weakmap来开辟存储空间,存储当前对象和拷贝对象的对应关系。
function deepClone(source, weakmap = new WeakMap()) {
// 判断是否为基本类型或者function,这里不对function做特别处理,可以直接返回
const sourceType = typeof source;
if ((sourceType !== 'object' && sourceType !== 'function') || source === null || sourceType === 'function') {
return source;
}
// 判断是否为Date,如果是,则新建一个Date类型的并返回
if (source.constructor === Date) {
return new Date(source);
}
// 判断是否为RegExp,如果是,则新建一个RegExp类型的并返回
if (source.constructor === RegExp) {
return new RegExp(source);
}
if (weakmap.has(source)) {
return weakmap.get(source);
}
let allDesc = Object.getOwnPropertyDescriptors(source);
// 判断是数组还是对象,并根据判断创建初始值,如果是对象,遍历传入参数所有键的特性
const target = Array.isArray(source) ? [] : Object.create(Object.getPrototypeOf(source), allDesc);
weakmap.set(source, target);
// 循环遍历source,利用Reflect.ownKeys()
Reflect.ownKeys(source).map((key) => {
if (typeof source[key] === 'object') {
target[key] = deepClone(source[key], weakmap);
} else {
target[key] = source[key];
}
})
return target;
}
Object.getOwnPropertyDescriptors()获取所指定对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
Object.getPrototypeOf()获取对象的原型,如果没有继承属性,就返回null。
Reflect.ownKeys()获取对象所有属性,包括不可遍历的属性,但是不包括继承的属性。
WeakMap利用它弱引用的特点,可以不用手动清除内存,有效防止内存泄漏,优化代码。。。这里也是利用它解决了循环引用的问题。
以下是测试代码
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.loop = obj // 设置loop成循环引用的属性
深浅拷贝要点
- 关于浅拷贝Object.assign()的用法与不足
- 自己写一个浅拷贝
- 深拷贝JSON.stringify()的用法与不足
- 自己写一个最终的深拷贝,注意weakMap和Reflect.ownKeys()。
对于这个深拷贝,理解还是不够深刻,涉及到的东西比较多,上述的实现只是目前自己的理解范围内的,但是还有许多问题,如function的拷贝等。。。并且需要多多复习,才能牢记并巩固。