一、概述
在JS开发中,我们常常需要复制或者克隆一个对象。对于简单数据类型,如数字、字符串、布尔值等,这是很容易实现的。但是对于复杂数据类型,如对象或者数组,就需要用到JS的深拷贝。深拷贝是指将一个对象完全复制一份,并开辟一个新的内存空间存放新对象,新对象的改动不会影响原对象。相比之下,JS的浅拷贝只是复制对象的引用,并没有开辟新的内存空间,因此改动新对象也会影响原对象。
二、基本实现方法
JS的深拷贝可以通过两种方法实现。
1. JSON.parse()
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
通过JSON.stringify()将对象转化为字符串,然后再通过JSON.parse()将字符串转化为新的对象,新对象就是原对象的深拷贝。这种方法的优点是实现简单,代码量小,但是缺点也显而易见:无法处理对象中含有函数、正则表达式等非JSON格式数据类型。而且该方法在处理大型对象的时候性能很低,因为需要进行两次序列化和反序列化操作。
2. 递归
function deepClone(obj, hash = new WeakMap()) {
if (Object(obj) !== obj) return obj;
if (hash.has(obj)) return hash.get(obj);
let clonedObj = new obj.constructor();
hash.set(obj, clonedObj);
if (obj instanceof Map) {
obj.forEach((value, key) => {
clonedObj.set(deepClone(key, hash), deepClone(value, hash));
});
} else if (obj instanceof Set) {
obj.forEach(value => {
clonedObj.add(deepClone(value, hash));
});
} else {
Object.getOwnPropertyNames(obj).forEach(prop => {
if (Object.getOwnPropertyDescriptor(obj, prop).enumerable) {
clonedObj[prop] = deepClone(obj[prop], hash);
}
});
}
return clonedObj;
}
递归方法则不会存在以上的问题,可以处理复杂对象,但是要注意对象中是否包含自引用的情况,因此需要引入哈希表记忆化处理。
三、对象中含有函数的深拷贝
使用递归方式进行深拷贝的时候,如果对象中含有函数,就会出现深拷贝失误的情况。因为递归过程中不会拷贝函数。因此需要单独处理对象中的函数,这可以分为两步:
1. 拷贝函数
let _toString = Object.prototype.toString;
let _hasOwnProperty = Object.prototype.hasOwnProperty;
function isFunction(obj) {
return _toString.call(obj) === '[object Function]' || typeof obj === 'function';
}
function cloneFunction(func) {
let bodyReg = /(?<={)(.|\n)+(?=})/m;
let paramReg = /(?<=\().+(?=\)\s+{)/;
let funcString = func.toString();
if (func.prototype) {
let params = paramReg.exec(funcString);
let body = bodyReg.exec(funcString);
if (body) {
if (params) {
params = params[0].split(',').map(param => param.trim());
return new Function(...params, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
function copy(obj, hash = new WeakMap()) {
if (Object(obj) !== obj) return obj;
if (hash.has(obj)) return hash.get(obj);
let type = _toString.call(obj);
let copiedObj;
switch (type) {
case '[object Array]':
copiedObj = new obj.constructor();
break;
case '[object Function]':
return cloneFunction(obj);
case '[object RegExp]':
copiedObj = new RegExp(obj.source, obj.flags);
break;
case '[object Date]':
copiedObj = new Date(obj.getTime());
break;
case '[object Set]':
copiedObj = new Set();
break;
case '[object Map]':
copiedObj = new Map();
break;
default:
copiedObj = new obj.constructor();
}
hash.set(obj, copiedObj);
if (obj instanceof Map) {
obj.forEach((value, key) => {
copiedObj.set(copy(key, hash), copy(value, hash));
});
} else if (obj instanceof Set) {
obj.forEach(value => {
copiedObj.add(copy(value, hash));
});
} else {
for (let prop in obj) {
if (_hasOwnProperty.call(obj, prop)) {
if (type === '[object Array]' || type === '[object Object]') {
copiedObj[prop] = copy(obj[prop], hash);
} else {
Object.defineProperty(copiedObj, prop, Object.getOwnPropertyDescriptor(obj, prop));
}
}
}
}
return copiedObj;
}
上述代码实现了函数的拷贝,其中cloneFunction()函数就是用于拷贝函数的。
2. 拷贝对象中的函数
function deepClone(obj, hash = new WeakMap()) {
if (Object(obj) !== obj) return obj;
if (hash.has(obj)) return hash.get(obj);
let clonedObj = copy(obj, hash);
hash.set(obj, clonedObj);
return clonedObj;
}
在进行递归遍历后,调用copy()方法对对象进行拷贝,并记录到哈希表中。这样即可实现深拷贝。
四、应用场景
JS的深拷贝主要用于解决对象的克隆和数据格式转化等问题。在以下场景中深拷贝会被广泛应用:
1. React组件之间的通信
在React组件内部,有时需要将一个组件的状态传递给子组件,这时候就需要使用深拷贝将状态复制一份,以防止子组件改变父组件的状态,从而避免数据污染的情况。
2. 前端缓存
在前端开发中,经常需要缓存一些数据到本地存储中。如果数据源是一个对象,使用深拷贝可以保证数据不会在缓存的过程中被修改,从而避免数据完整性的问题。
3. 数据结构的复制
在JS中,集合类数据结构(例如Map,Set等)是常用的存储数据的方式。在使用时,需要对这些数据结构进行复制,以便进行修改、删除等操作。使用深拷贝可以确保复制后的数据和原数据完全独立,防止由于互相引用导致结果不可预知。
4. 数据类型转化
在开发中,有时需要将一个数据格式转换成另一个格式。例如将一个JSON对象转换为XML或CSV格式,可以通过先将JSON对象深拷贝得到一个JS对象,然后再将JS对象转换为需要的格式。
五、总结
JS的深拷贝对于复制对象是一项非常关键的技术,掌握深拷贝的方法有助于提高代码效率和数据完整性。本文介绍了两种实现深拷贝的方法,以及如何处理对象中的函数,同时对深拷贝的应用场景作了介绍。