前言
大家好,我是林三心。前幾天跟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
結語
如果你覺得此文對你有一丁點幫助,點個贊,鼓勵一下林三心哈哈。或者可以加入我的摸魚群,我們一起好好學習啊啊啊啊啊啊啊,我會定時模擬面試,簡歷指導,答疑解惑,我們們互相學習共同進步!!