使用JavaScript實現深拷貝
1.JSON序列化實現深拷貝
在JS中,想要對某一個物件(引用型別)進行一次簡單的深拷貝,可以使用JSON提供給我們的兩個方法。
JSON.stringfy()
:可以將JavaScript型別轉成對應的JSON字串;JSON.parse()
:可以解析JSON,將其轉回對應的JavaScript型別;
具體深拷貝的實現:
const obj = {
name: 'curry',
age: 30,
friends: ['kobe', 'klay'],
playBall() {
console.log('Curry is playing basketball.')
}
}
const newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
列印結果:
JSON序列化實現深拷貝的優缺點:
- 如果只是對一個簡單物件進行深拷貝,那麼使用該方法是很方便的;
- 但根據上面的列印結果可以發現,原
obj
的方法屬性並沒有被拷貝到newObj
中; - JSON序列化只能對普通物件進行深拷貝,如果物件中包含函式、undefined、Symbol等型別的值是無能為力的,會直接將其忽略掉;
2.自定義深拷貝函式
既然上面的方法不能滿足我們的需求,那麼就自己來一步步實現一個深拷貝函式吧。
2.1.基本功能實現
- 實現深拷貝基本功能,暫時先不對特殊型別進行處理;
- 定義一個輔助函式
isObject
,用於判斷傳入資料是否是物件型別;
function isObject(value) {
const valueType = typeof value
// 值不能為null,並且為物件或者函式型別
return (value !== null) && (valueType === 'object' || valueType === 'function')
}
function deepClone(originValue) {
// 判斷傳入的是否是物件型別,如果不是,說明是普通型別的值,直接返回即可
if (!isObject(originValue)) {
return originValue
}
const newObj = {} // 定義一個空物件
// 迴圈遍歷物件,取出key和值存放到空物件中
// 注意:for...in遍歷物件會將其繼承的屬性也遍歷出來,所以需要加hasOwnProperty進行判斷是否是自身的屬性
for (const key in originValue) {
if (originValue.hasOwnProperty(key)) {
// 遞迴呼叫deepClone,如果物件屬性值中還包含物件,就會再次進行拷貝處理
newObj[key] = deepClone(originValue[key])
}
}
// 深拷貝完成,將得到新物件返回
return newObj
}
簡單測試一下:
const obj = {
name: 'curry',
age: 30,
friends: {
name: 'klay',
age: 11
}
}
const newObj = deepClone(obj)
console.log(newObj)
console.log(newObj.friends === obj.friends)
列印結果:
2.2.其他型別處理
- 對其它資料型別進行處理,如陣列、函式、Symbol、Set、Map等;
- 對函式型別的判斷,直接返回該函式即可,因為函式本身就是可以複用的;
- Symbol不僅可以作為value,還可以作為key,需要對key為Symbol型別的情況進行處理;
function deepClone(originValue) {
// 1.判斷傳入的是否是一個函式型別
if (typeof originValue === 'function') {
// 將函式直接返回即可
return originValue
}
// 2.判斷傳入的是否是一個Map型別
if (originValue instanceof Map) {
return new Map([...originValue])
}
// 3.判斷傳入的是否是一個Set型別
if (originValue instanceof Set) {
return new Set([...originValue])
}
// 4.判斷傳入的值是否是一個Symbol型別
if (typeof originValue === 'symbol') {
// 返回一個新的Symbol,並且將其描述傳遞過去
return Symbol(originValue.description)
}
// 5.判斷傳入的值是否是一個undefined
if (typeof originValue === 'undefined') {
return undefined
}
// 6.判斷傳入的是否是物件型別,如果不是,說明是普通型別的值,直接返回即可
if (!isObject(originValue)) {
return originValue
}
// 7.定義一個變數,如果傳入的是陣列就定義為一個陣列
const newValue = Array.isArray(originValue) ? [] : {}
// 8.迴圈遍歷,如果是物件,就取出key和值存放到空物件中,如果是陣列,就去除下標和元素放到空陣列中
// 注意:for...in遍歷物件會將其繼承的屬性也遍歷出來,所以需要加hasOwnProperty進行判斷是否是自身的屬性
for (const key in originValue) {
if (originValue.hasOwnProperty(key)) {
// 遞迴呼叫deepClone,如果物件屬性值中還包含物件,就會再次進行拷貝處理
newValue[key] = deepClone(originValue[key])
}
}
// 9.對key為Symbol型別的情況進行處理
// 拿到所有為Symbol型別的key
const symbolKeys = Object.getOwnPropertySymbols(originValue)
// for...of遍歷取出所有的key,存放到新物件中
for (const sKey of symbolKeys) {
newValue[sKey] = deepClone(originValue[sKey])
}
// 10.深拷貝完成,將得到新物件返回
return newValue
}
簡單測試一下:
const s1 = Symbol('aaa')
const s2 = Symbol('bbb')
const obj = {
name: 'curry',
age: undefined,
friends: {
name: 'klay',
age: 11
},
hobbies: ['籃球', '足球', '高爾夫'],
map: new Map([[1, 'aaa'], [2, 'bbb'], [3, 'ccc']]),
set: new Set([1, 2, 3]),
s: s1,
[s2]: 'abc'
}
const newObj = deepClone(obj)
console.log(newObj)
列印結果:
2.3.迴圈引用處理
我們自定義深拷貝的函式是通過遞迴來實現的,如果物件中有一個屬性值指向了自己,那麼在進行深拷貝時會陷入無限迴圈,這種情況也就是迴圈引用。
如果沒有處理迴圈引用,那麼就會不斷遞迴,最終報錯棧溢位:
- 迴圈引用的處理,只需要拿到新建立的物件返回即可,所以必須將這個新物件儲存下來,在遇到迴圈引用屬性時,直接就可以拿到;
- Map和WeakMap都可以實現對物件進行儲存,這裡使用WeakMap進行儲存,原因是WeakMap對物件的引用是弱引用;
- 只需要將原物件作為WeakMap中的key,其值對應存放我們新建立出來的物件即可,下一次遞迴時進行判斷WeakMap中是否存有該物件,如果有就取出返回;
function deepClone(originValue, wMap = new WeakMap()) {
// 1.判斷傳入的是否是一個函式型別
if (typeof originValue === 'function') {
// 將函式直接返回即可
return originValue
}
// 2.判斷傳入的是否是一個Map型別
if (originValue instanceof Map) {
return new Map([...originValue])
}
// 3.判斷傳入的是否是一個Set型別
if (originValue instanceof Set) {
return new Set([...originValue])
}
// 4.判斷傳入的值是否是一個Symbol型別
if (typeof originValue === 'symbol') {
// 返回一個新的Symbol,並且將其描述傳遞過去
return Symbol(originValue.description)
}
// 5.判斷傳入的值是否是一個undefined
if (typeof originValue === 'undefined') {
return undefined
}
// 6.判斷傳入的是否是物件型別,如果不是,說明是普通型別的值,直接返回即可
if (!isObject(originValue)) {
return originValue
}
// 迴圈引用處理:判斷wMap中是否存在原物件,如果存在就取出原物件對應的新物件返回
if (wMap.has(originValue)) {
return wMap.get(originValue)
}
// 7.定義一個變數,如果傳入的是陣列就定義為一個陣列
const newValue = Array.isArray(originValue) ? [] : {}
// 迴圈引用處理:將原物件作為key,新物件作為value,存入wMap中
wMap.set(originValue, newValue)
// 8.迴圈遍歷,如果是物件,就取出key和值存放到空物件中,如果是陣列,就去除下標和元素放到空陣列中
// 注意:for...in遍歷物件會將其繼承的屬性也遍歷出來,所以需要加hasOwnProperty進行判斷是否是自身的屬性
for (const key in originValue) {
if (originValue.hasOwnProperty(key)) {
// 遞迴呼叫deepClone,如果物件屬性值中還包含物件,就會再次進行拷貝處理
newValue[key] = deepClone(originValue[key], wMap)
}
}
// 9.對key為Symbol型別的情況進行處理
// 拿到所有為Symbol型別的key
const symbolKeys = Object.getOwnPropertySymbols(originValue)
// for...of遍歷取出所有的key,存放到新物件中
for (const sKey of symbolKeys) {
newValue[sKey] = deepClone(originValue[sKey], wMap)
}
// 10.深拷貝完成,將得到新物件返回
return newValue
}
簡單測試一下:
const s1 = Symbol('aaa')
const s2 = Symbol('bbb')
const obj = {
name: 'curry',
age: undefined,
friends: {
name: 'klay',
age: 11
},
hobbies: ['籃球', '足球', '高爾夫'],
map: new Map([[1, 'aaa'], [2, 'bbb'], [3, 'ccc']]),
set: new Set([1, 2, 3]),
s: s1,
[s2]: 'abc'
}
// 迴圈引用
obj.self = obj
const newObj = deepClone(obj)
console.log(newObj)
console.log(newObj.self.self.self.self)
列印結果: