1. 背景:为什么有必要掌握深拷贝?
在业务中经常会遇到需要对模板数据 / 初始数据进行加工处理,而且,该数据可能能需要在多处被复用。
简单粗暴更改原始数据的做法,会污染其他依赖项,所以我们需要对原始数据进行一份深拷贝,保持各个依赖项数据的独立性,做到既可以复用,又不互相污染。
2. JS 中没有自带的深拷贝 API
也许我们用到过 Object.assign() 进行对象合并,亦或者用数组的 slice(), concat() 方法对数组进行复制,但这几种方法都不是深拷贝,是浅拷贝。
简言之,深拷贝就是每个对象都是独立的,独立意味着拥有各自的独立内存空间;而浅拷贝意味着拥有公共的引用空间。
所以,值类型的数据不存在浅拷贝的问题,只有引用数据类型才存在。愿更深入的了解深拷贝和浅拷贝的,可以参考文章最末尾的「参考文章」。
3. 对于数组的深拷贝,一个很简单的做法:
let recursiveClone = val => Array.isArray(val) ? Array.from(val, recursiveClone) : val;
4. 封装:对于任意一个 JS 数据类型的深拷贝
概述 JS 的数据类型,从大致上分为两种:
- 基本数据类型:string, number, boolean, undefined, null, symbol
- 引用数据类型: object(这是一个统称,包含 object, array, function 以及 js 单体内置对象如 Date, RegExp, Error 等)
对于基本数据类型,不存在深拷贝的问题,因为它们是值类型的数据。值类型的数据存放在栈内存中,重新赋值就是独立的。
而对于众多的引用数据类型,需要分别进行处理,集中处理 object 和 array,这也是我们在业务中遇到最多的情况。
5. 上代码:
const deepCloneTypes = ['Object', 'Array', 'Map', 'Set'];
function isObject(source) {
const type = typeof source;
return source !== null && (type === 'object' || type === 'function')
}
function getType(source) {return (Object.prototype.toString.call(source)).split(' ')[1].slice(0, -1)
}
function processOtherType(source) {
const Ctor = source.constructor;
return new Ctor(source);
}
function processFunctionType (source) {let _source = source.toString();
// 区分是否是箭头函数
if (source.prototype) {
// 如果有 prototype 就是普通函数
let argsReg = /function\s*\w*\(([^\)]*)\)/;
let bodyReg = /\{([\s\S]*)\}/;
let fnArgs = (argsReg.exec(source))[1];
let fnBody = (bodyReg.exec(source))[1];
console.log(fnArgs, fnBody);
return new Function (fnArgs, fnBody)
} else {
// 箭头函数没有 prototype
return eval(_source)
}
}
function deepClone (source, map = new WeakMap()) {
// 首先用 typeof 来筛选是否是引用数据类型,如果连引用数据类型都不是,那么就判断是基本数据类型,直接返回即可
if (!isObject(source)) {return source}
const type = getType(source);
let cloneTarget;
// 防止循环引用
if (map.get(source)) {return map.get(source);
}
map.set(source, cloneTarget);
// 接下来判断是否是需要进行循环拷贝的引用数据类型,诸如 new Boolean, new Number 这样的,也不需要循环拷贝
if (!deepCloneTypes.includes(type)) {cloneTarget = processOtherType(source);
} else {cloneTarget = new source.constructor();
// return Object.create(source.constructor.prototype) // 不能这样,这样是创造了一个对象
if (type === 'Object' || type === 'Array') {let keys = type === 'Object' ? Object.keys(source) : undefined; // 如果支持 optional chaining 的话可以写成?.
(keys || source).forEach((val, key) => {if (keys) {key = val;}
cloneTarget[key] = deepClone(source[key], map); // 在这里进行递归调用
})
}
if (type === 'Function') {cloneTarget = processFunctionType(source)
}
if (type === 'Map') {source.forEach((val, key) => {cloneTarget.set(key, deepClone(val, map))
})
}
if (type === 'Set') {source.forEach((val, key) => {cloneTarget.add(deepClone(val, map))
})
}
}
return cloneTarget
}
参考:
如何写出一个惊艳面试官的深拷贝?
JavaScript 深入了解基本类型和引用类型的值
Array.from() 五个超好用的用途