前言
大家好,我是林三心。前几天跟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) // falseconsole.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) // falseconsole.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)}// 判断援用类型的tempfunction 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...),我会定时直播模仿面试,答疑解惑