话不多说,我们来一步步推导如何实现一个深拷贝!!! 既然要深拷贝,哪我们一定需要知道哪些数据类型是无需拷贝,直接复制就能用的。 众所周知,js的基本数据类型有八种。
分别是undefined
、null
、number
、boolean
、string
、symbol
而需要进行深拷贝 的类型是引用类型 。
引用类型有Array
、Object
、Map
、Set
、Function
、Date
、RegExp
、WeakMap
、WeakSet
等
为了知道深拷贝函数传入的值是什么类型,我们先实现一个能完美判断数据类型的辅助函数
完美判断数据的辅助函数
javascript
// 完美判断数据类型的函数
function getType(obj) {
let objType = Object.prototype.toString.call(obj)
return objType
}
// 基本数据类型
console.log(getType(1)) // [object Number]
console.log(getType("Hello")) // [object String]
console.log(getType(true)) // [object Boolean]
console.log(getType(Symbol("World"))) // [object Symbol]
console.log(getType(undefined)) // [object Undefined]
console.log(getType(null)) // [object Null]
console.log(getType(NaN)) // [object Number]
console.log(getType(Infinity)) //[object Number]
// 引用数据类型
console.log(getType({})) // [object Object]
console.log(getType([])) // [object Array]
console.log(getType(new Map())) // [object Map]
console.log(getType(new Set())) // [object Set]
console.log(getType(new WeakMap())) // [object WeakMap]
console.log(getType(new WeakSet())) // [object WeakSet]
console.log(getType(new Promise(() => { }))) // [object Promise]
console.log(getType(new Function())) // [object Function]
console.log(getType(new ArrayBuffer(8))) // [object ArrayBuffer]
console.log(getType(new Uint8Array(8))) // [object Uint8Array]
console.log(getType(new RegExp())) // [object RegExp]
console.log(getType(new Date())) // [object Date]
所以说我们现在来一步步拷贝这些奇奇怪怪的引用类型 先写一个名为deepClone
的函数来,确定好它的输入输出
javascript
/**
* 疯不皮写的深拷贝
* @param {被拷贝的值} oldObject
* @returns newObject 深拷贝完成的值
*/
function deepClone(oldObject){
let newObject;
return newObject
}
给看本文的同学提一个醒,现在需要你脑中形成一个意识。就是,这个函数其实什么都可以进行深拷贝,传入什么引用类型的数据就再创建一个新的这个玩意。然后再把旧的值塞回去就是深拷贝了。
我们暂且不考虑循环引用 和拷贝对象嵌套对象 的情况。 我们先考虑如何随便传入一个数据类型就能拷贝一个新的数据类型出来
一、拷贝基本数据类型
拷贝基本类型就很简单了,直接把旧对象oldObject
的基本数据类型的值取出来赋值就行。但是 ,作为方法的封装者是无法知道使用者会拿这个方法放入什么值来进行深拷贝的。所以,我们需要先判断输入的值是否是引用类型,如果不是引用类型直接返回出去就行了。
javascript
function deepClone(oldObject) {
// 最后的结果
let newObject;
// 使用getType方法获取数据类型
const oldObjectType = getType(oldObject)
// 第一步拷贝基本数据类型
// 1、判断是否为基本数据类型
if (typeof oldObject !== 'object' || oldObjectType === '[object Null]') {
newObject = oldObject
}
return newObject
}
二、拷贝数组
拷贝数组也很简单,只需要一个for循环把所有可遍历的数据塞给一个新数组就行了
javascript
function deepClone(oldObject) {
// 最后的结果
...
// 使用getType方法获取数据类型
...
// 一、拷贝基本数据类型
// 1、判断是否为基本数据类型
...
// 二、拷贝数组
if (oldObjectType === '[object Array]') {
newObject = []
for (const key in oldObject) {
newObject[key] = oldObject[key];
}
}
return newObject
}
但如果数组中嵌套了新的对象呢? 直接for循环再赋值进来的值可不是深拷贝哟,因此需要递归处理一下
javascript
function deepClone(oldObject) {
// 一、拷贝基本数据类型
...
// 二、拷贝数组
if (oldObjectType === '[object Array]') {
newObject = []
for (const key in oldObject) {
// 旧的:newObject[key] = oldObject[key];
newObject[key] = deepClone(oldObject[key]); // 新的返回一个被深拷贝了的数据类型
}
}
return newObject
}
为什么修改成这个样子呢?我来讲解一下:
这是一种名为递归 的算法,当执行栈执行到
newObject[key] = deepClone(oldObject[key])
时,就会再一次的进入 这个名为deepClone
的函数中。 但是,原来deepClone函数没有执行的方法,将会继续保留在执行栈 中。 比如上述代码还没执行return newObject
。就会等待newObject[key] = deepClone(oldObject[key])
中的deepClone(oldObject[key])
返回值之后,再执行原先deepClone
方法没有执行完的同步代码。 目的是为了能拷贝嵌套的第二层数据。由于我们确定了该函数的输入输出 ,进来的是某个数据类型 ,出来的 是一个被深拷贝了 的数据类型 。 因此,此处不会发生错误!!!
三、拷贝普通对象
如果使用者给deepClone
方法传入的是一个对象,那么我们还需要处理一下对象中可能的Symbol
属性。
javascript
function deepClone(oldObject) {
let newObject = {};
// 第一步 拷贝基本数据类型
// 1、判断是否为基本数据类型null
...
// 2、把旧对象的值拷贝到新对象中
...
// 第二步 拷贝数组对象
// 1、判断是否为数组
...
// 三、拷贝普通对象(不拷贝这个普通对象上面的原型对象)
if (oldObjectType === '[object Object]') {
// 获取所有的键(包括symbol类型的键)
newObject = {}; // 初始化新对象
// 获取一个对象中所有所有所有的键
const oldObjectKeys = Object.getOwnPropertySymbols(oldObject).concat(Object.keys(oldObject))
for (const oldObjectKey of oldObjectKeys) {
newObject[oldObjectKey] = deepClone(oldObject[oldObjectKey])//这里也递归了的哟~
}
}
return newObject
}
- 通过
Object.getOwnPropertySymbols(oldObject)
方法获取对象中的所有Symbol
属性的值,在和使用了Object.keys(oldObject)
方法返回的数组进行拼接。获取这个对象第一层所有所有所有所有的键
最基本的对象和数组都拷贝完啦,现在我们来随便扩充点想拷贝的类型在后面吧~
四、拷贝map类型
javascript
function deepClone(oldObject) {
let newObject = {};
// 一、拷贝基本数据类型
// 二、拷贝数组对象
// 三、拷贝普通对象(不拷贝这个普通对象上面的原型对象)
...
// 四、拷贝map类型
if (oldObjectType === '[object Map]') {
const newMap = new Map();
for (const [key, value] of oldObject) {
newMap.set(key, deepClone(value))
}
newObject = newMap
}
return newObject
}
五、拷贝set类型
javascript
function deepClone(oldObject) {
let newObject = {};
// 一、拷贝基本数据类型
// 二、拷贝数组对象
// 三、拷贝普通对象(不拷贝这个普通对象上面的原型对象)
// 四、拷贝map类型
...
// 五、拷贝set类型
if (oldObjectType === '[object Set]') {
const newSet = new Set();
for (const setItem of oldObject) {
newSet.add(deepClone(setItem));
}
newObject = newSet;
}
return newObject
}
六、拷贝函数
javascript
function deepClone(oldObject) {
let newObject = {};
// 一、拷贝基本数据类型
// 二、拷贝数组对象
// 三、拷贝普通对象(不拷贝这个普通对象上面的原型对象)
// 四、拷贝map类型
// 五、拷贝set类型
...
// 六、拷贝函数
if (oldObjectType === '[object Function]') {
newObject = oldObject
}
return newObject
}
七、拷贝symbol类型的数据
虽然这个玩意不是引用类型的,但是因为symbol类型的特殊性。我们还是需要对他进行深拷贝一份。
注意一下下,此处拷贝的是 symbol类型的值 ,而是不是 对象中的键
javascript
function deepClone(oldObject) {
let newObject = {};
// 一、拷贝基本数据类型
...
// 七、拷贝值为symbol类型的数据
if (oldObjectType === '[object Symbol]') {
newObject = Symbol(oldObject.description)
}
return newObject
}
八、拷贝时间类型
javascript
function deepClone(oldObject) {
let newObject = {};
// 一、拷贝基本数据类型
...
// 八、拷贝时间类型
if (oldObjectType === '[object Date]') {
newObject = new Date(oldObject)
}
return newObject
}
九、拷贝正则类型
javascript
function deepClone(oldObject) {
let newObject = {};
// 一、拷贝基本数据类型
...
// 九、拷贝正则类型
if (oldObjectType === '[object RegExp]') {
newObject = new RegExp(oldObject)
}
return newObject
}
十、拷贝WeakSet类型
javascript
function deepClone(oldObject) {
let newObject = {};
// 一、拷贝基本数据类型
...
// 十、拷贝WeakSet类型
if (oldObjectType === '[object WeakSet]') {
const newWeakSet = new WeakSet()
for (const item of oldObject) {
newWeakSet.add(deepClone(item));
}
newObject = newWeakSet;
}
return newObject
}
十一、拷贝WeakMap类型
javascript
function deepClone(oldObject) {
let newObject = {};
// 一、拷贝基本数据类型
...
// 十一、拷贝WeakMap类型
if (oldObjectType === '[object WeakMap]') {
const newWeakMap = new WeakMap()
for (const [key, value] of oldObject) {
newWeakMap.set(key, deepClone(value))
}
newObject = newWeakMap
}
return newObject
}
自此基本上所有常见的类型 均进行了深拷贝!!! 什么你还想加其他类型,你看我上面写了这么多,不就是新开个对象然后把值塞进去就完了嘛!? 但是!我们还没解决对象循环引用的问题!!!
解决对象的循环引用问题
现在把我们之前写的拷贝普通对象的代码取过来。
javascript
function deepClone(oldObject) {
let newObject = {};
// 第一步 拷贝基本数据类型
// 第二步 拷贝数组对象
...
// 三、拷贝普通对象(不拷贝这个普通对象上面的原型对象)
if (oldObjectType === '[object Object]') {
// 获取所有的键(包括symbol类型的键)
newObject = {}; // 初始化新对象
// 获取一个对象中所有所有所有的键
const oldObjectKeys = Object.getOwnPropertySymbols(oldObject).concat(Object.keys(oldObject))
for (const oldObjectKey of oldObjectKeys) {
newObject[oldObjectKey] = deepClone(oldObject[oldObjectKey])//这里也递归了的哟~
}
}
return newObject
}
开动你的小脑瓜子,你思考一下。假如,你要拷贝的对象引用了自己,我们是不是只需要把已经拷贝过后的值也直接引用自己不就行了吗? 哪该如何保证递归到第二层之后,我们还能知道上一层的值呢? 答案是!我们使用一个名为weekmap的类型缓存一份值!!!
使用WeakMap作为缓存,究其原因是因为它是弱引用,方便销毁。并且weekmap类型的键只能是对象,值可以是任意的。
上代码!
javascript
function deepClone(oldObject,weakMap = new WeakMap() // 新加的 ) {
let newObject = {};
// 第一步 拷贝基本数据类型
// 第二步 拷贝数组对象
...
// 三、拷贝普通对象(不拷贝这个普通对象上面的原型对象)
if (oldObjectType === '[object Object]') {
newObject = {}; // 初始化新对象
// 缓存一份结果,防止对象嵌套对象进行循环引用
if (weakMap.has(oldObject)) { // 新加的
newObject = weakMap.get(oldObject) // 新加的
} else { // 缓存没有再进去获取 // 新加的
// 先缓存一下上次deepClone的结果 // 新加的
weakMap.set(oldObject, newObject) // 新加的
// 获取所有的键(包括symbol类型的键)
const oldObjectKeys = Object.getOwnPropertySymbols(oldObject).concat(Object.keys(oldObject))
for (const oldObjectKey of oldObjectKeys) {
newObject[oldObjectKey] = deepClone(oldObject[oldObjectKey], weakMap)
}
} // 新加的
}
return newObject
}
我知道肯定有很多同学看不懂这个。比如为什么weakMap.set(oldObject, newObject)
方法为什么要放在for循环上方,明明newObject
还是个{}
啊?不应该放在下面吗?可是,为什么放在下面了还是递归爆栈了啊? 我一步步解答:
- 你可以尝试在for循环之前打印一次
weakmap
看看是不是想象中的newObject
是个{}
- 既然不是,是什么导致这个
newObject
的值居然跟oldObject
一样的? - 你再把
for循环
给注释了欸,看看在for循环
之前的weakmap
是什么。 - 居然变成
undefined
了?!未执行的代码居然影响了之前的打印!? - 其实,究其本质是因为
console.log()
在打印复杂数据类型的时候是异步。在weakmap
中,因为newObject
是一个引用类型,内部的值会被for循环
填充。
自此我们完完全全的实现了一个半完美的深拷贝的函数!!!(真正完满的深拷贝还是lodash库内的哪个深拷贝是最完美的)
所有的代码
javascript
// 完美判断数据类型的函数
function getType(obj) {
let objType = Object.prototype.toString.call(obj)
return objType
}
function deepClone(oldObject, weakMap = new WeakMap()) {
// 最后的结果
let newObject;
// 获取数据类型
const oldObjectType = getType(oldObject)
// 一、拷贝基本数据类型
// 判断是否为基本数据类型
if (typeof oldObject !== 'object' || oldObjectType === '[object Null]') {
newObject = oldObject
}
// 二、拷贝数组
if (oldObjectType === '[object Array]') {
newObject = []
for (const key in oldObject) {
newObject[key] = deepClone(oldObject[key]);
}
}
// 三、拷贝普通对象(不拷贝这个普通对象上面的原型对象)
if (oldObjectType === '[object Object]') {
newObject = {}; // 初始化新对象
// 缓存一份结果,防止对象嵌套对象进行循环引用
if (weakMap.has(oldObject)) {
newObject = weakMap.get(oldObject)
} else { // 缓存没有再进去获取
// 先缓存一下上次deepClone的结果
weakMap.set(oldObject, newObject)
// 获取所有的键(包括symbol类型的键)
const oldObjectKeys = Object.getOwnPropertySymbols(oldObject).concat(Object.keys(oldObject))
for (const oldObjectKey of oldObjectKeys) {
newObject[oldObjectKey] = deepClone(oldObject[oldObjectKey], weakMap)
}
}
}
// 四、拷贝map类型
if (oldObjectType === '[object Map]') {
const newMap = new Map();
for (const [key, value] of oldObject) {
newMap.set(key, deepClone(value))
}
newObject = newMap
}
// 五、拷贝set类型
if (oldObjectType === '[object Set]') {
const newSet = new Set();
for (const setItem of oldObject) {
newSet.add(deepClone(setItem));
}
newObject = newSet;
}
// 六、拷贝函数
if (oldObjectType === '[object Function]') {
newObject = oldObject
}
// 七、拷贝值为symbol类型的数据
if (oldObjectType === '[object Symbol]') {
newObject = Symbol(oldObject.description)
}
// 八、拷贝时间类型
if (oldObjectType === '[object Date]') {
newObject = new Date(oldObject)
}
// 九、拷贝正则类型
if (oldObjectType === '[object RegExp]') {
newObject = new RegExp(oldObject)
}
// 十、拷贝WeakSet类型
if (oldObjectType === '[object WeakSet]') {
const newWeakSet = new WeakSet()
for (const item of oldObject) {
newWeakSet.add(deepClone(item));
}
newObject = newWeakSet;
}
// 十一、拷贝WeakMap类型
if (oldObjectType === '[object WeakMap]') {
const newWeakMap = new WeakMap()
for (const [key, value] of oldObject) {
newWeakMap.set(key, deepClone(value))
}
newObject = newWeakMap
}
return newObject
}
但是吧,谁家手写深拷贝这么大一坨啊~ 砸门,不写多了。就假定使用者只传数组和对象进来。现在我们再看怎么写。
javascript
function deepClone(oldObject, weakMap = new WeakMap()) {
let newObject;
// 获取数据类型
const oldObjectType = Object.prototype.toString.call(oldObject)
// 拷贝数组
if (oldObjectType === '[object Array]') {
newObject = []
for (const key in oldObject) {
newObject[key] = oldObject[key];
}
}
// 拷贝对象
if (oldObjectType === '[object Object]') {
newObject = {}; // 初始化新对象
// 缓存一份结果,防止对象嵌套对象进行循环引用
if (weakMap.has(oldObject)) {
newObject = weakMap.get(oldObject)
} else { // 缓存没有再进去获取
// 先缓存一下上次deepClone的结果
weakMap.set(oldObject, newObject)
// 获取所有的键(包括symbol类型的键)
const oldObjectKeys = Object.getOwnPropertySymbols(oldObject).concat(Object.keys(oldObject))
for (const oldObjectKey of oldObjectKeys) {
newObject[oldObjectKey] = deepClone(oldObject[oldObjectKey], weakMap)
}
}
}
return newObject
}
这样看简单易懂,就是把其他数据类型的处理删掉就行了,但是还是太多了~ 我们继续压缩一下,我们只需要处理对象和数组就行了
超级无敌精简版
javascript
function deepClone(oldObject, map = new WeakMap()) {
if (map.has(oldObject)) return map.get(oldObject)
const newObject = Array.isArray(oldObject) ? [] : {}
map.set(oldObject, newObject)
for (const oldObjectkey in oldObject) {
newObject[key] = deepClone(oldObject[key], map)
}
return newObject
}
你看简单多了是不~