本文介绍简单一下深浅拷贝手写版,重点解决深拷贝的循环引用和及函数引用相同问题, 会的大佬请绕过
一、数据类型
1、基本数据类型
number、string、boolean、null、undefined、symbol、bigint
bigint 是最近新引入的基本数据类型
2、引用数据类型
Object、Array、Function等
数据类型不是本文重点, 重点是实现深浅拷贝
function deepCopy(obj, weakMap = new WeakMap()) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
if (obj instanceof Date) {
return new Date(obj);
}
if (weakMap.has(obj)) {
return weakMap.get(obj);
}
let copy;
if (Array.isArray(obj)) {
copy = [];
weakMap.set(obj, copy);
obj.forEach((item, index) => {
copy[index] = deepCopy(item, weakMap);
});
} else {
copy = {};
weakMap.set(obj, copy);
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], weakMap);
});
}
// 处理函数
if (typeof obj === "function") {
// 使用 eval 将函数字符串转换为函数对象
copy = eval(`(${obj.toString()})`);
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], weakMap);
});
}
return copy;
}
二、 浅拷贝
1、什么是浅拷贝
一句话可以说就是:对对象而言,它的第一层属性值如果是基本数据类型则完全拷贝一份数据,如果是引用类型就拷贝内存地址。确实拷贝的很浅[偷笑]
2、实现
Object.assign()
let obj1 = {
name: 'yang',
res: {
value: 123
}
}
let obj2 = Object.assign({}, obj1)
obj2.res.value = 456
console.log(obj2) // {name: "yang", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}
obj2.name = 'haha'
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}
展开语法 Spread
let obj1 = {
name: 'yang',
res: {
value: 123
}
}
let {...obj2} = obj1
obj2.res.value = 456
console.log(obj2) // {name: "yang", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}
obj2.name = 'haha'
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}
Array.prototype.slice
const arr1 = [
'yang',
{
value: 123
}
];
const arr2 = arr1.slice(0);
arr2[1].value = 456;
console.log(arr2); // ["yang", {value: 456}]
console.log(arr1); // ["yang", {value: 456}]
arr2[0] = 'haha';
console.log(arr2); // ["haha", {value: 456}]
console.log(arr1); // ["yang", {value: 456}]
Array.prototype.concat
const arr1 = [
'yang',
{
value: 123
}
];
const arr2 = [].concat(arr1);
arr2[1].value = 456;
console.log(arr2); // ["yang", {value: 456}]
console.log(arr1); // ["yang", {value: 456}]
arr2[0] = 'haha';
console.log(arr2); // ["haha", {value: 456}]
console.log(arr1); // ["yang", {value: 456}]
实际上对于数组来说, 只要不修改原数组, 重新返回一个新数组就可以实现浅拷贝,比如说map、filter、reduce等方法
三、深拷贝
1、什么是深拷贝
深拷贝就是不管是基本数据类型还是引用数据类型都重新拷贝一份, 不存在共用数据的现象
2、实现
暴力版本 JSON.parse(JSON.stringify(object))
let obj = JSON.parse(JSON.stringify($obj))
console.log(obj) // 不能解决循环引用
/*
VM348:1 Uncaught TypeError: Converting circular structure to JSON
at JSON.stringify (<anonymous>)
at <anonymous>:1:17
*/
delete $obj.c.d
let obj = JSON.parse(JSON.stringify($obj))
console.log(obj) // 丢失了大部分属性
/*
{
a: null
c: {a: 1}
date: "2020-04-05T09:51:32.610Z"
e: {}
f: {}
}
*/
存在的问题:
1、会忽略 undefined
2、会忽略 symbol
3、不能序列化函数
4、不能解决循环引用的对象
5、不能正确处理new Date()
6、不能处理正则
7、不能处理new Error()
第一版 基础版本
递归遍历对象属性
function deepCopy (obj) {
if (obj === null || typeof obj !== 'object') {
return obj
}
let copy = Array.isArray(obj) ? [] : {}
Object.keys(obj).forEach(v => {
copy[key] = deepCopy(obj[key])
})
return copy
}
deepCopy($obj)
/*
VM601:23 Uncaught RangeError: Maximum call stack size exceeded
at <anonymous>:23:30
at Array.forEach (<anonymous>)
at deepCopy (<anonymous>:23:22)
*/
delete $obj.c.d
deepCopy($obj)
/*
{
a: null
b: undefined
c: {a: 1}
date: {}
e: {}
f: {}
func: ƒ ()
symbol: Symbol()
}
*/
存在的问题是:
1、不能解决循环引用的对象
2、不能正确处理new Date()
3、不能处理正则
4、不能处理new Error()
第二版 解决循环引用
先解决解决循环遍历问题, 解决办法是将对象,对象属性存储在数组中查看下次遍历时有无已经遍历过的对象,有则直接返回, 否则继续遍历
function deepCopy (obj, cache = []) {
if (obj === null || typeof obj !== 'object') {
return obj
}
const item = cache.filter(item => item.original === obj)[0]
if (item) return item.copy
let copy = Array.isArray(obj) ? [] : {}
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
deepCopy($obj)
/*
{
a: null
b: undefined
c: {a: 1, d: {…}}
date: {}
e: {}
f: {}
func: ƒ ()
symbol: Symbol()
}
完美解决了循环引用问题, 但是仍然存在几个小问题,都属于同一类问题
第三版 解决特殊值
对于最终的几个对象的处理,可以判断类型, 重新new一个返回就可以了
function deepCopy (obj, cache = []) {
if (obj === null || typeof obj !== 'object') {
return obj
}
if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
const item = cache.filter(item => item.original === obj)[0]
if (item) return item.copy
let copy = Array.isArray(obj) ? [] : {}
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
deepCopy($obj)
/*
{
a: null
b: undefined
c: {a: 1, d: {…}}
date: Fri Apr 10 2020 20:06:08 GMT+0800 (中国标准时间) {}
e: /regexp/
f: Error: Error: error at deepCopy (<anonymous>:8:74) at <anonymous>:19:21 at Array.forEach (<anonymous>) at deepCopy (<anonymous>:18:22) at <anonymous>:24:1
func: ƒ ()
symbol: Symbol()
}
*/
除此之外Promise、Set、Map、WeakSet、WeakMap也可以用相同的办法处理
第四版 解决函数引用相同
到这里基本功能似乎已经实现, 但是还存在一个问题, 就是函数引用了同一个内存地址, 对于这个问题网上大部分都是直接返回或者返回为对象, 包括lodash也是这么处理的
const isFunc = typeof value == 'function'
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
那么如何结局这个问题就需要用eval函数, 虽说这个函数已经不推荐使用, 但是它还是能解决问题的。 函数这里分两种: 普通函数和箭头函数, 区分这两者只需要看有无prototype, 有prototype属性就属于普通函数, 没有就是箭头函数。
普通函数属于函数声明, 不能直接使用eval, 需要用小括号包起来形成函数表达式, 而箭头函数本身就是函数表达式
// lmran
function copyFunction(func) {
let fnStr = func.toString()
return func.prototype ? eval(`(${fnStr})`) : eval(fnStr)
}
function deepCopy (obj, cache = []) {
if (typeof obj === 'function') {
return copyFunction(obj)
}
if (obj === null || typeof obj !== 'object') {
return obj
}
if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
const item = cache.filter(item => item.original === obj)[0]
if (item) return item.copy
let copy = Array.isArray(obj) ? [] : {}
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
deepCopy($obj).func === $obj.func // false
总结
至此, 深浅拷贝已全部实现,但学无止境,我们还可以考虑使用 Proxy提升来深拷贝性能,通过拦截 set 和 get 实现,当然 Object.defineProperty() 也可以。感兴趣的同学可以查看这文章,文章介绍的很详细头条面试官:你知道如何实现高性能版本的深拷贝嘛?
解疑
针对@Brota掘友提出的对于函数引用相同的问题, 文中现已修复, 但如果函数是内置函数代码仍有问题, 现给出解决办法, 如果函数是内置函数直接返回。但是又绕回到了函数引用相同, 内置函数本身就存在一份,所以引用地址相同也就没什么问题
function copyFunction(func) {
let fnStr = func.toString()
if (fnStr === `function ${func.name}() { [native code] }`) {
return func
}
return func.prototype ? eval(`(${fnStr})`) : eval(fnStr)
}
目前还未实现的功能:闭包的处理, 暂未找到处理办法,有知道的可以在下面留言评论
优化版
function deepCopy(obj, weakMap = new WeakMap()) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
if (obj instanceof Date) {
return new Date(obj);
}
if (weakMap.has(obj)) {
return weakMap.get(obj);
}
let copy;
if (Array.isArray(obj)) {
copy = [];
weakMap.set(obj, copy);
obj.forEach((item, index) => {
copy[index] = deepCopy(item, weakMap);
});
} else {
copy = {};
weakMap.set(obj, copy);
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], weakMap);
});
}
// 处理函数
if (typeof obj === "function") {
// 使用 eval 将函数字符串转换为函数对象
copy = eval(`(${obj.toString()})`);
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], weakMap);
});
}
return copy;
}