共计 7433 个字符,预计需要花费 19 分钟才能阅读完成。
前言
大家好,我是林三心。前几天跟 leader 在聊 深拷贝
- leader:你晓得怎么复制一个对象吗?
- 我:晓得啊!不就
深拷贝
吗? - leader:那你是怎么
深拷贝
的? - 我:我间接一手
JSON.parse(JSON.stringfy(obj))
吃遍天 - leader:兄弟,有空去看看
lodash
里的deepClone
,看看人家是怎么实现的
哈哈,的确,深拷贝
在日常开发中是有很多利用场景的,他也是十分重要的,写一个合格的 深拷贝
办法是很有必要的。那怎么能力写一个合格的 深拷贝
办法呢?或者说,怎么能力写一个毫无漏洞的 深拷贝
办法呢?
深拷贝 && 浅拷贝
咱们先来说说什么是深拷贝,什么是浅拷贝吧。
浅拷贝
所谓浅拷贝,就是只复制最外一层,外面的都还是雷同援用
// 浅拷贝
const a = {name: 'sunshine_lin', age: 23, arr: [] }
const b = {}
for (let key in a){b[key] = a[key]
}
console.log(b) // {name: 'sunshine_lin', age: 23, arr: [] }
console.log(b === a) // false
console.log(b.arr === a.arr) // true
深拷贝
深拷贝,则是你将一个对象拷贝到另一个新变量,这个新变量指向的是一块新的堆内存地址
// 深拷贝
function deepClone(target) {// ... 实现深拷贝}
const a = {name: 'sunshine_lin', age: 23, arr: [] }
const b = deepClone(a)
console.log(b) // {name: 'sunshine_lin', age: 23, arr: [] }
console.log(b === a) // false
console.log(b.arr === a.arr) // false
黄金版本
置信大多数人平时在实现深拷贝时,都会这么去实现
function deepClone(target) {return JSON.parse(JSON.stringify(target))
}
const a = {name: 'sunshine_lin', age: 23}
const b = deepClone(a)
console.log(b) // {name: 'sunshine_lin', age: 23}
console.log(b === a) // false
尽管大多数时候这么应用是没问题的,但这种形式还是有很多毛病的
- 1、对象中有字段值为
undefined
,转换后则会间接字段隐没 - 2、对象如果有字段值为
RegExp
对象,转换后则字段值会变成{} - 3、对象如果有字段值为
NaN、+-Infinity
,转换后则字段值变成 null - 4、对象如果有
环援用
,转换间接报错
铂金版本
既然是要对象的深拷贝,那我能够创立一个空对象,并把须要拷贝的原对象的值一个一个复制过去就能够了呀!!!
function deepClone(target) {const temp = {}
for (const key in target) {temp[key] = target[key]
}
return temp
}
const a = {name: 'sunshine_lin', age: 23}
const b = deepClone(a)
console.log(b) // {name: 'sunshine_lin', age: 23}
console.log(b === a) // false
然而其实下面这种做法是不欠缺的,因为咱们基本不晓得咱们想拷贝的对象有多少层。。大家一听到“不晓得有多少层”,想必就会想到递归了吧,是的,应用递归就能够了。
function deepClone(target) {
// 根本数据类型间接返回
if (typeof target !== 'object') {return target}
// 援用数据类型非凡解决
const temp = {}
for (const key in target) {
// 递归
temp[key] = deepClone(target[key])
}
return temp
}
const a = {
name: 'sunshine_lin',
age: 23,
hobbies: {sports: '篮球',tv: '雍正王朝'}
}
const b = deepClone(a)
console.log(b)
// {
// name: 'sunshine_lin',
// age: 23,
// hobbies: {sports: '篮球', tv: '雍正王朝'}
// }
console.log(b === a) // false
钻石版本
后面咱们只思考了对象的状况,然而没把数组状况也给思考,所以咱们要加上数组条件
function deepClone(target) {
// 根本数据类型间接返回
if (typeof target !== 'object') {return target}
// 援用数据类型非凡解决
// 判断数组还是对象
const temp = Array.isArray(target) ? [] : {}
for (const key in target) {
// 递归
temp[key] = deepClone(target[key])
}
return temp
}
const a = {
name: 'sunshine_lin',
age: 23,
hobbies: {sports: '篮球', tv: '雍正王朝'},
works: ['2020', '2021']
}
const b = deepClone(a)
console.log(b)
// {
// name: 'sunshine_lin',
// age: 23,
// hobbies: {sports: '篮球', tv: '雍正王朝'},
// works: ['2020', '2021']
// }
console.log(b === a) // false
星耀版本
后面实现的办法都没有解决 环援用
的问题
JSON.parse(JSON.stringify(target))
报错TypeError: Converting circular structure to JSON
,意思是无奈解决环援用
递归办法
报错Maximum call stack size exceeded
,意思是递归不完,爆栈
// 环援用
const a = {}
a.key = a
那怎么解决环援用呢?其实说难也不难,须要用到 ES6 的数据结构Map
- 每次遍历到有援用数据类型,就把他当做
key
放到Map
中,对应的value
是新创建的对象 temp
- 每次遍历到有援用数据类型,就去 Map 中找找有没有对应的
key
,如果有,就阐明这个对象之前曾经注册过,当初又遇到第二次,那必定就是环援用了,间接依据key
获取value
,并返回value
function deepClone(target, map = new Map()) {
// 根本数据类型间接返回
if (typeof target !== 'object') {return target}
// 援用数据类型非凡解决
// 判断数组还是对象
const temp = Array.isArray(target) ? [] : {}
+ if (map.get(target)) {
+ // 已存在则间接返回
+ return map.get(target)
+ }
+ // 不存在则第一次设置
+ map.set(target, temp)
for (const key in target) {
// 递归
temp[key] = deepClone(target[key], map)
}
return temp
}
const a = {
name: 'sunshine_lin',
age: 23,
hobbies: {sports: '篮球', tv: '雍正王朝'},
works: ['2020', '2021']
}
a.key = a // 环援用
const b = deepClone(a)
console.log(b)
// {
// name: 'sunshine_lin',
// age: 23,
// hobbies: {sports: '篮球', tv: '雍正王朝'},
// works: ['2020', '2021'],
// key: [Circular]
// }
console.log(b === a) // false
王者版本
刚刚咱们只是实现了
根本数据类型
的拷贝援用数据类型
中的数组,对象
但其实,援用数据类型可不止只有数组和对象,咱们还得解决以下的援用类型的拷贝问题,那怎么判断每个援用数据类型的各自类型呢?能够应用Object.prototype.toString.call()
类型 | toString | 后果 |
---|---|---|
Map | Object.prototype.toString.call(new Map()) | [object Map] |
Set | Object.prototype.toString.call(new Set()) | [object Set] |
Array | Object.prototype.toString.call([]) | [object Array] |
Object | Object.prototype.toString.call({}) | [object Object] |
Symbol | Object.prototype.toString.call(Symbol()) | [object Symbol] |
RegExp | Object.prototype.toString.call(new RegExp()) | [object RegExp] |
Function | Object.prototype.toString.call(function() {}) | [object Function] |
咱们先把以上的援用类型数据分为两类
- 可遍历的数据类型
- 不可遍历的数据类型
// 可遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
// 不可遍历类型
const symbolTag = '[object Symbol]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
// 将可遍历类型存在一个数组里
const canForArr = ['[object Map]', '[object Set]',
'[object Array]', '[object Object]']
// 将不可遍历类型存在一个数组
const noForArr = ['[object Symbol]', '[object RegExp]', '[object Function]']
// 判断类型的函数
function checkType(target) {return Object.prototype.toString.call(target)
}
// 判断援用类型的 temp
function checkTemp(target) {
const c = target.constructor
return new c()}
可遍历援用类型
次要解决以下四种类型
- Map
- Set
- Object
-
Array
function deepClone(target, map = new Map()) {
-
const type = checkType(target)
// 根本数据类型间接返回
- if (!canForArr.concat(noForArr).includes(type)) {
- return target
-
}
// 援用数据类型非凡解决
-
const temp = checkTemp(target)
if (map.get(target)) {
// 已存在则间接返回 return map.get(target)
}
// 不存在则第一次设置
map.set(target, temp)// 解决 Map 类型
- if (type === mapTag) {
- target.forEach((value, key) => {
- temp.set(key, deepClone(value, map))
- })
+ - return temp
-
}
// 解决 Set 类型
- if (type === setTag) {
- target.forEach(value => {
- temp.add(deepClone(value, map))
- })
+ - return temp
-
}
// 解决数据和对象
for (const key in target) {// 递归 temp[key] = deepClone(target[key], map)
}
return temp
}const a = {
name: ‘sunshine_lin’,
age: 23,
hobbies: {sports: ‘ 篮球 ’, tv: ‘ 雍正王朝 ’},
works: [‘2020’, ‘2021’],
map: new Map([[‘haha’, 111], [‘xixi’, 222]]),
set: new Set([1, 2, 3]),
}
a.key = a // 环援用
const b = deepClone(a)console.log(b)
// {
// name: ‘sunshine_lin’,
// age: 23,
// hobbies: {sports: ‘ 篮球 ’, tv: ‘ 雍正王朝 ’},
// works: [‘2020’, ‘2021’],
// map: Map {‘haha’ => 111, ‘xixi’ => 222},
// set: Set {1, 2, 3},
// key: [Circular]
// }
console.log(b === a) // false
不可遍历援用类型
次要解决以下几种类型
- Symbol
- RegExp
- Function
先把拷贝这三个类型的办法写进去
// 拷贝 Function 的办法
function cloneFunction(func) {const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {if (param) {const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {return new Function(body[0]);
}
} else {return null;}
} else {return eval(funcString);
}
}
// 拷贝 Symbol 的办法
function cloneSymbol(targe) {return Object(Symbol.prototype.valueOf.call(targe));
}
// 拷贝 RegExp 的办法
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
最终版本
function deepClone(target, map = new Map()) {
// 获取类型
const type = checkType(target)
// 根本数据类型间接返回
if (!canForArr.concat(noForArr).includes(type)) return target
// 判断 Function,RegExp,Symbol
+ if (type === funcTag) return cloneFunction(target)
+ if (type === regexpTag) return cloneReg(target)
+ if (type === symbolTag) return cloneSymbol(target)
// 援用数据类型非凡解决
const temp = checkTemp(target)
if (map.get(target)) {
// 已存在则间接返回
return map.get(target)
}
// 不存在则第一次设置
map.set(target, temp)
// 解决 Map 类型
if (type === mapTag) {target.forEach((value, key) => {temp.set(key, deepClone(value, map))
})
return temp
}
// 解决 Set 类型
if (type === setTag) {
target.forEach(value => {temp.add(deepClone(value, map))
})
return temp
}
// 解决数据和对象
for (const key in target) {
// 递归
temp[key] = deepClone(target[key], map)
}
return temp
}
const a = {
name: 'sunshine_lin',
age: 23,
hobbies: {sports: '篮球', tv: '雍正王朝'},
works: ['2020', '2021'],
map: new Map([['haha', 111], ['xixi', 222]]),
set: new Set([1, 2, 3]),
func: (name, age) => `${name}往年 ${age}岁啦!!!`,
sym: Symbol(123),
reg: new RegExp(/haha/g),
}
a.key = a // 环援用
const b = deepClone(a)
console.log(b)
// {
// name: 'sunshine_lin',
// age: 23,
// hobbies: {sports: '篮球', tv: '雍正王朝'},
// works: ['2020', '2021'],
// map: Map {'haha' => 111, 'xixi' => 222},
// set: Set {1, 2, 3},
// func: [Function],
// sym: [Symbol: Symbol(123)],
// reg: /haha/g,
// key: [Circular]
// }
console.log(b === a) // false
结语
如果你感觉此文对你有一丁点帮忙,点个赞,激励一下林三心哈哈。或者能够退出我的摸鱼群
想进学习群,摸鱼群,请点击这里[摸鱼](
https://juejin.cn/pin/6969565…),我会定时直播模仿面试,答疑解惑