# 自定义深拷贝函数
- 对象相互赋值的一些关系,分别包括:
- 引入的赋值:指向同一个对象,相互之间会影响;
- 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
- 对象的深拷贝:两个对象不再有任何关系,不会相互影响;
- 我们可以通过一种方法来实现深拷贝了:JSON.parse
- 这种深拷贝的方式其实对于函数、Symbol 等是无法处理的;
- 并且如果存在对象的循环引用,也会报错的;
- 自定义深拷贝函数:
- 自定义深拷贝的基本功能
- 对 Symbol 的 key 进行处理
- 其他数据类型的值进程处理:数组、函数、Symbol、Set、Map
- 对循环引用的处理
# JSON 实现伪深拷贝
- 拿 lain 对象举栗
const symbol = Symbol() | |
const lain = { | |
name: "lain", | |
friend: { | |
name: "saber" | |
}, | |
foo() { | |
console.log("foo~") | |
}, | |
[symbol]: symbol, | |
symbol: symbol | |
} |
- 使用
JSON
进行深拷贝
const newLain = JSON.parse(JSON.stringify(lain)) | |
console.log(lain) | |
/* | |
{ | |
name: 'lain', | |
friend: { name: 'saber' }, | |
foo: [Function: foo], | |
symbol: Symbol(), | |
[Symbol()]: Symbol() | |
} | |
*/ | |
console.log(newLain) // { name: 'lain', friend: { name: 'saber' } } |
-
从上面输出可以看出是有很多弊端的
- 对函数是没办法处理的
- 对 Symbol 也是无法处理的
- 并且无法将
lain
指向自己
lain.lain = lain // 从逻辑上看是没有问题的,但实际上这样会报错
# deepClone v1 基本实现
if (!(isObject(originValue))) return originValue
这句代码最主要是用来终止deepClone
函数的递归调用
// 用于判断是否是 对象类型或者函数类型 | |
function isObject(value) { | |
const valueType = typeof value | |
return (valueType !== null && typeof value === 'object' || typeof value === 'function' ) | |
} | |
function deepClone(originValue) { | |
// 如果不是对象类型则直接将当前值返回 | |
if (!(isObject(originValue))) return originValue | |
const newObject = {} | |
for (const key in originValue) { | |
newObject[key] = deepClone(originValue[key]) | |
} | |
return newObject | |
} |
- 测试对象
const lain = { | |
name: "lain", | |
friend: { | |
name: "saber", | |
fruits: ['cherry', 'peack', 'watermelon'], | |
friend: { | |
name: '樱岛麻衣', | |
foo() { | |
console.log(`樱岛麻衣 foo~`); | |
}, | |
}, | |
foo() { | |
console.log("saber foo~") | |
} | |
}, | |
foo() { | |
console.log("lain foo~") | |
}, | |
} |
- 测试代码
const newLain = deepClone(lain) | |
console.log(lain) | |
/* | |
{ | |
name: 'lain', | |
friend: { | |
name: 'saber', | |
fruits: ['cherry', 'peack', 'watermelon'], | |
friend: { name: ' 樱岛麻衣 ', foo: [Function: foo] }, | |
foo: [Function: foo] | |
}, | |
foo: [Function: foo] | |
} | |
*/ | |
console.log(newLain) | |
/* | |
{ | |
name: 'lain', | |
friend: { | |
name: 'saber', | |
fruits: {'0': 'cherry', '1': 'peack', '2': 'watermelon'}, | |
friend: { name: ' 樱岛麻衣 ', foo: {} }, | |
foo: {} | |
}, | |
foo: {} | |
} | |
*/ |
- 从上面来看确实是实现了深拷贝,但是限制新对象与原对象是有些许差异的
- 比如数组
fruits
,原对象是一个数组,有三个元素,而新对象的fruits
已经变为了一个对象,用索引作为key
,用value
作为值 - 还有函数也变为了对象
- 没有对
Symbol
类型进行处理 - 也没有对
Set/Map
进行处理
- 比如数组
- 所以接下来我会进一步进行完善深拷贝的其他类型判断
# deepClone v2 其他类型
- 这里进行了非常多的判断逻辑,都是比较简单的
- 可能对于
Symbol/Set/Map
类型的深拷贝会稍微复杂些
// 这里重写了用 is 对象来判断类型 | |
const is = { | |
Array: Array.isArray, | |
Date: (val) => val instanceof Date, | |
Set: (val) => Object.prototype.toString.call(val) === '[object Set]', | |
Map: (val) => Object.prototype.toString.call(val) === '[object Map]', | |
Object: (val) => Object.prototype.toString.call(val) === '[object Object]', | |
Symbol: (val) => Object.prototype.toString.call(val) === '[object Symbol]', | |
Function: (val) => Object.prototype.toString.call(val) === '[object Function]', | |
} | |
function deepClone(value) { | |
// 2.1 函数浅拷贝 | |
/* if (is.Function(value)) return value */ | |
// 2.2 函数深拷贝 | |
if (is.Function(value)) { | |
if (/^function/.test(value.toString()) || /^\(\)/.test(value.toString())) | |
return new Function('return ' + value.toString())() | |
return new Function('return function ' + value.toString())() | |
} | |
// 3.Date 深拷贝 | |
if (is.Date(value)) return new Date(value.valueOf()) | |
// 4. 判断如果是 Symbol 的 value, 那么创建一个新的 Symbol | |
if (is.Symbol(value)) return Symbol(value.description) | |
// 5. 判断是否是 Set 类型 进行深拷贝 | |
if (is.Set(value)) { | |
// 5.1 浅拷贝 直接进行解构即可 | |
// return new Set([...value]) | |
// 5.2 深拷贝 | |
const newSet = new Set() | |
for (const item of value) newSet.add(deepClone(item)) | |
return newSet | |
} | |
// 6. 判断是否是 Map 类型 | |
if (is.Map(value)) { | |
// 6.1 浅拷贝 直接进行解构即可 | |
// return new Map([...value]) | |
// 6.2 深拷贝 | |
const newMap = new Map() | |
for (const item of value) newMap.set(deepClone(item[0]), deepClone(item[1])) | |
return newMap | |
} | |
// 1. 如果不是对象类型则直接将当前值返回 | |
if (!(is.Object(value))) return value | |
// 7. 判断传入的对象是数组,还是对象 | |
const newObject = is.Array(value) ? [] : {} | |
for (const key in value) { | |
// 8 进行递归调用 | |
newObject[key] = deepClone(value[key]) | |
} | |
// 4.1 对 Symbol 作为 key 进行特殊的处理 拿到对象上面的所有 Symbol key,以数组形式返回 | |
const symbolKeys = Object.getOwnPropertySymbols(value) | |
for (const sKey of symbolKeys) { | |
// 4.2 这里没有必要创建一个新的 Symbol | |
// const newSKey = Symbol(sKey.description) | |
// 4.3 直接将原来的 Symbol key 拷贝到新对象上就可以了 | |
newObject[sKey] = deepClone(value[sKey]) | |
} | |
return newObject | |
} |
# 测试目标对象
const symboLain = Symbol('lain') | |
const symboSaber = Symbol('saber') | |
const lain = { | |
name: "lain", | |
friend: { | |
name: "saber", | |
fruits: ['cherry', 'peack', 'watermelon'], | |
friend: { | |
name: '樱岛麻衣', | |
foo() { | |
console.log(`樱岛麻衣 foo~`); | |
}, | |
}, | |
foo() { | |
console.log("saber foo~") | |
} | |
}, | |
foo() { | |
console.log("lain foo~") | |
}, | |
[new Date]: new Date(), | |
[symboLain]: symboLain, | |
[symboSaber]: symboSaber, | |
symboLain: symboSaber, | |
set: new Set(['入间同学', '蝶祈', '枫', {a: 1, b: 2}]), | |
map: new Map([['age1', 16], ['age2', 17], ['age3', 18], ['obj', { a: 1, b: 2 }]]), | |
} |
# 测试 Function
console.log(lain.foo === newLain.foo) // false | |
console.log(lain.friend.foo === newLain.friend.foo) // false | |
console.log(lain.friend.friend.foo === newLain.friend.friend.foo) // false |
# 测试 Date
console.log(lain[new Date]) // 可自行测试 | |
console.log(newLain[new Date]) // 时间也与上面一样 可自行测试 | |
console.log(lain[new Date] === newLain[new Date]) // false |
# 测试 Symbol
console.log(lain) | |
/* | |
{ | |
name: 'lain', | |
friend: { | |
name: 'saber', | |
fruits: ['cherry', 'peack', 'watermelon'], | |
friend: { name: ' 樱岛麻衣 ', foo: [Function: foo] }, | |
foo: [Function: foo] | |
}, | |
foo: [Function: foo], | |
'Sun Feb 13 2022 14:52:05 GMT+0800 (中国标准时间)': 2022-02-13T06:52:05.892Z, | |
symboLain: Symbol (saber), | |
set: Set (4) { ' 入间同学 ', ' 蝶祈 ', ' 枫 ', { a: 1, b: 2 } }, | |
map: Map (4) { | |
'age1' => 16, | |
'age2' => 17, | |
'age3' => 18, | |
'obj' => { a: 1, b: 2 } | |
}, | |
[Symbol (lain)]: Symbol (lain), | |
[Symbol (saber)]: Symbol (saber) | |
} | |
*/ | |
console.log(newLain) | |
/* | |
{ | |
name: 'lain', | |
friend: { | |
name: 'saber', | |
fruits: ['cherry', 'peack', 'watermelon'], | |
friend: { name: ' 樱岛麻衣 ', foo: [Function: foo] }, | |
foo: [Function: foo] | |
}, | |
foo: [Function: foo], | |
'Sun Feb 13 2022 14:52:05 GMT+0800 (中国标准时间)': 2022-02-13T06:52:05.892Z, | |
symboLain: Symbol (saber), | |
set: Set (4) { ' 入间同学 ', ' 蝶祈 ', ' 枫 ', { a: 1, b: 2 } }, | |
map: Map (4) { | |
'age1' => 16, | |
'age2' => 17, | |
'age3' => 18, | |
'obj' => { a: 1, b: 2 } | |
}, | |
[Symbol (lain)]: Symbol (lain), | |
[Symbol (saber)]: Symbol (saber) | |
} | |
*/ |
# 测试 Set
lain.set.forEach(item => { | |
if (is.Object(item)) { | |
// 这里对 lain 中 set 属性中的对象进行添加属性 | |
item.c = 3 | |
} | |
}) | |
console.log(lain.set) // Set (4) { ' 入间同学 ', ' 蝶祈 ', ' 枫 ', { a: 1, b: 2, c: 3 } } | |
console.log(newLain.set) // Set (4) { ' 入间同学 ', ' 蝶祈 ', ' 枫 ', { a: 1, b: 2 } } |
# 最后对 Map
进行代码测试
lain.map.forEach(item => { | |
if (is.Object(item)) { | |
// 这里对 lain 中 map 属性中的对象进行添加属性 | |
item.c = 3 | |
} | |
}) | |
console.log(lain.map) | |
/* | |
Map(4) { | |
'age1' => 16, | |
'age2' => 17, | |
'age3' => 18, | |
'obj' => { a: 1, b: 2, c: 3 } | |
} | |
*/ | |
console.log(newLain.map) | |
/* | |
Map(4) { | |
'age1' => 16, | |
'age2' => 17, | |
'age3' => 18, | |
'obj' => { a: 1, b: 2 } | |
} | |
*/ |
-
map
也是没有问题的 -
那么类型篇章算是完结了,但我们此时的代码是有
bug
的,至于是什么bug
,看下面标题应该就知道了~
# deepClone v3 循环引用
- 此时我们进行一步操作,用下面的代码对上面对象进行测试会发生什么呢?
lain.lain = lain | |
const newLain = deepClone(lain) |
Uncaught RangeError: Maximum call stack size exceeded
没错,会发生栈溢出!
// 用于判断类型 | |
const is = { | |
Array: Array.isArray, | |
Date: (val) => val instanceof Date, | |
Set: (val) => Object.prototype.toString.call(val) === '[object Set]', | |
Map: (val) => Object.prototype.toString.call(val) === '[object Map]', | |
Object: (val) => Object.prototype.toString.call(val) === '[object Object]', | |
Symbol: (val) => Object.prototype.toString.call(val) === '[object Symbol]', | |
Function: (val) => Object.prototype.toString.call(val) === '[object Function]', | |
} | |
function deepClone(value, weakMap = new WeakMap()) { | |
// 2.1 函数浅拷贝 | |
/* if (is.Function(value)) return value */ | |
// 2.2 函数深拷贝 | |
if (is.Function(value)) { | |
if (/^function/.test(value.toString()) || /^\(\)/.test(value.toString())) | |
return new Function('return ' + value.toString())() | |
return new Function('return function ' + value.toString())() | |
} | |
// 3.Date 深拷贝 | |
if (is.Date(value)) return new Date(value.valueOf()) | |
// 4. 判断如果是 Symbol 的 value, 那么创建一个新的 Symbol | |
if (is.Symbol(value)) return Symbol(value.description) | |
// 5. 判断是否是 Set 类型 进行深拷贝 | |
if (is.Set(value)) { | |
// 5.1 浅拷贝 直接进行解构即可 | |
// return new Set([...value]) | |
// 5.2 深拷贝 | |
const newSet = new Set() | |
for (const item of value) newSet.add(deepClone(item), weakMap) | |
return newSet | |
} | |
// 6. 判断是否是 Map 类型 | |
if (is.Map(value)) { | |
// 6.1 浅拷贝 直接进行解构即可 | |
// return new Map([...value]) | |
// 6.2 深拷贝 | |
const newMap = new Map() | |
for (const item of value) newMap.set(deepClone(item[0], weakMap), deepClone(item[1], weakMap)) | |
return newMap | |
} | |
// 9. 判断 weakMap 是否有值 有值的情况下就直接将值返回就可以 | |
if(weakMap.has(value)) return weakMap.get(value) | |
// 1. 如果不是对象类型则直接将当前值返回 | |
if (!(is.Object(value))) return value | |
// 7. 判断传入的对象是数组,还是对象 | |
const newObj = is.Array(value) ? [] : {} | |
// 10. 当 weakMap 没有值时,将 originValue 作为 key, newObj 作为 value | |
weakMap.set(value, newObj) | |
for (const key in value) { | |
weakMap.set(value, newObj) | |
// 8 进行递归调用 | |
newObj[key] = deepClone(value[key], weakMap) | |
} | |
// 4.1 对 Symbol 作为 key 进行特殊的处理 拿到对象上面的所有 Symbol key,以数组形式返回 | |
const symbolKeys = Object.getOwnPropertySymbols(value) | |
for (const sKey of symbolKeys) { | |
// 4.2 这里没有必要创建一个新的 Symbol | |
// const newSKey = Symbol(sKey.description) | |
// 4.3 直接将原来的 Symbol key 拷贝到新对象上就可以了 | |
newObj[sKey] = deepClone(value[sKey], weakMap) | |
} | |
return newObj | |
} |
- 继续用上面的代码进行测试,建议用
浏览器测试
lain.lain = lain | |
const newLain = deepClone(lain) | |
// 没有溢栈 可自行去测 | |
console.log(lain) | |
console.log(newLain) |
- 此时代码就没有问题了,那么深拷贝篇到此就快结束了~
# deepClone v4 仿源码版
- 阅读了 vuex 源码并自己实现了 vuex 后,我也对深拷贝进阶的实现了下
function isFunction(val) { | |
return Object.prototype.toString.call(val) === '[object Function]' | |
} | |
function isObject(val) { | |
return Object.prototype.toString.call(val) === '[object Object]' | |
} | |
function isArray(val) { | |
return Object.prototype.toString.call(val) === '[object Array]' | |
} | |
function isSet(val) { | |
return Object.prototype.toString.call(val) === '[object Set]' | |
} | |
function isMap(val) { | |
return Object.prototype.toString.call(val) === '[object Map]' | |
} | |
function isSymbol(val) { | |
return Object.prototype.toString.call(val) === '[object Symbol]' | |
} | |
function isDate(val) { | |
return Object.prototype.toString.call(val) === '[object Date]' | |
} | |
function ArrayBuffer(val) { | |
return Object.prototype.toString.call(val) === '[object ArrayBuffer]' | |
} | |
const forEachValue = (obj, fn) => Object.keys(obj).forEach(key => fn(obj[key], key)) | |
function deepClone(val, weakMap = new WeakMap()) { | |
if (isDate(val)) return new Date(+val) | |
if (isMap(val)) { | |
const map = new Map() | |
for (const item of val) map.set(deepClone(item[0], weakMap), deepClone(item[1], weakMap)) | |
return map | |
} | |
if (isSet(val)) { | |
const set = new Set() | |
val.forEach(item => set.add(deepClone(item), weakMap)) | |
return set | |
} | |
if (isSymbol(val)) return Symbol(val.description) | |
if (isFunction(val)) { | |
if (/^function|^\(\)/.test(val.toString())) { | |
return new Function(`return ${val.toString()}`)() | |
} else { | |
return new Function(`return function ${val.toString()}`)() | |
} | |
} | |
if (!isObject(val)) return val | |
const obj = isArray(val) ? [] : {} | |
if(weakMap.has(val)) return weakMap.get(val) | |
weakMap.set(val, obj) | |
forEachValue(val, (val, key) => obj[key] = deepClone(val, weakMap)) | |
const symbols = Object.getOwnPropertySymbols(val) | |
forEachValue(symbols, key => obj[Symbol(key.description)] = deepClone(symbols[key], weakMap)) | |
return obj | |
} |