前端(vue)入門到精通課程:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:
網上有很多關於深拷貝的文章,但是品質良莠不齊,有很多都考慮得不周到,寫的方法比較簡陋,難以令人滿意。本文旨在完成一個完美的深拷貝,大家看了如果有問題,歡迎一起補充完善。
評價一個深拷貝是否完善,請檢查以下問題是否都實現了:
基本型別資料
是否能拷貝?
鍵和值都是基本型別的普通物件
是否能拷貝?
Symbol
作為物件的key是否能拷貝?
Date
和RegExp
物件型別是否能拷貝?
Map
和Set
物件型別是否能拷貝?
Function
物件型別是否能拷貝?(函數我們一般不用深拷貝)
物件的原型
是否能拷貝?
不可列舉屬性
是否能拷貝?
迴圈參照
是否能拷貝?
怎樣?你寫的深拷貝夠完善嗎?
這裡先直接給出最終的程式碼版本,方便想快速瞭解的人檢視,當然,你想一步步瞭解可以繼續檢視文章餘下的內容:
function deepClone(target) {
const map = new WeakMap()
function isObject(target) {
return (typeof target === 'object' && target ) || typeof target === 'function'
}
function clone(data) {
if (!isObject(data)) {
return data
}
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data)
}
if (typeof data === 'function') {
return new Function('return ' + data.toString())()
}
const exist = map.get(data)
if (exist) {
return exist
}
if (data instanceof Map) {
const result = new Map()
map.set(data, result)
data.forEach((val, key) => {
if (isObject(val)) {
result.set(key, clone(val))
} else {
result.set(key, val)
}
})
return result
}
if (data instanceof Set) {
const result = new Set()
map.set(data, result)
data.forEach(val => {
if (isObject(val)) {
result.add(clone(val))
} else {
result.add(val)
}
})
return result
}
const keys = Reflect.ownKeys(data)
const allDesc = Object.getOwnPropertyDescriptors(data)
const result = Object.create(Object.getPrototypeOf(data), allDesc)
map.set(data, result)
keys.forEach(key => {
const val = data[key]
if (isObject(val)) {
result[key] = clone(val)
} else {
result[key] = val
}
})
return result
}
return clone(target)
}
登入後複製
先看看JS資料型別圖(除了Object
,其他都是基礎型別):
在JavaScript中,基礎型別值的複製是直接拷貝一份新的一模一樣的資料,這兩份資料相互獨立,互不影響。而參照型別值(Object型別)的複製是傳遞物件的參照(也就是物件所在的記憶體地址,即指向物件的指標),相當於多個變數指向同一個物件,那麼只要其中的一個變數對這個物件進行修改,其他的變數所指向的物件也會跟著修改(因為它們指向的是同一個物件)。如下圖:
深淺拷貝主要針對的是Object型別,基礎型別的值本身即是複製一模一樣的一份,不區分深淺拷貝。這裡我們先給出測試的拷貝物件,大家可以拿這個obj
物件來測試一下自己寫的深拷貝函數是否完善:
// 測試的obj物件
const obj = {
// =========== 1.基礎資料型別 ===========
num: 0, // number
str: '', // string
bool: true, // boolean
unf: undefined, // undefined
nul: null, // null
sym: Symbol('sym'), // symbol
bign: BigInt(1n), // bigint
// =========== 2.Object型別 ===========
// 普通物件
obj: {
name: '我是一個物件',
id: 1
},
// 陣列
arr: [0, 1, 2],
// 函數
func: function () {
console.log('我是一個函數')
},
// 日期
date: new Date(0),
// 正則
reg: new RegExp('/我是一個正則/ig'),
// Map
map: new Map().set('mapKey', 1),
// Set
set: new Set().add('set'),
// =========== 3.其他 ===========
[Symbol('1')]: 1 // Symbol作為key
};
// 4.新增不可列舉屬性
Object.defineProperty(obj, 'innumerable', {
enumerable: false,
value: '不可列舉屬性'
});
// 5.設定原型物件
Object.setPrototypeOf(obj, {
proto: 'proto'
})
// 6.設定loop成迴圈參照的屬性
obj.loop = obj
登入後複製
obj
物件在Chrome瀏覽器中的結果:
淺拷貝: 建立一個新的物件,來接受你要重新複製或參照的物件值。如果物件屬性是基本的資料型別,複製的就是基本型別的值給新物件;但如果屬性是參照資料型別,複製的就是記憶體中的地址,如果其中一個物件改變了這個記憶體中的地址所指向的物件,肯定會影響到另一個物件。
首先我們看看一些淺拷貝的方法(詳細瞭解可點選對應方法的超連結):
方法 | 使用方式 | 注意事項 |
---|---|---|
Object.assign() | Object.assign(target, ...sources) 說明:用於將所有可列舉屬性的值從一個或多個源物件分配到目標物件。它將返回目標物件。 | 1.不會拷貝物件的繼承屬性; 2.不會拷貝物件的不可列舉的屬性; 3.可以拷貝 Symbol 型別的屬性。 |
展開語法 | let objClone = { ...obj }; | 缺陷和Object.assign() 差不多,但是如果屬性都是基本型別的值,使用擴充套件運運算元進行淺拷貝會更加方便。 |
Array.prototype.concat()拷貝陣列 | const new_array = old_array.concat(value1[, value2[, ...[, valueN]]]) | 淺拷貝,適用於基本型別值的陣列 |
Array.prototype.slice()拷貝陣列 | arr.slice([begin[, end]]) | 淺拷貝,適用於基本型別值的陣列 |
這裡只列舉了常用的幾種方式,除此之外當然還有其他更多的方式。注意,我們直接使用=
賦值不是淺拷貝,因為它是直接指向同一個物件了,並沒有返回一個新物件。
手動實現一個淺拷貝:
function shallowClone(target) {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
// 測試
const shallowCloneObj = shallowClone(obj)
shallowCloneObj === obj // false,返回的是一個新物件
shallowCloneObj.arr === obj.arr // true,對於物件型別只拷貝了參照
登入後複製
從上面這段程式碼可以看出,利用型別判斷(檢視typeof),針對參照型別的物件進行 for 迴圈遍歷物件屬性賦值給目標物件的屬性(for...in
語句以任意順序遍歷一個物件的除Symbol以外的可列舉屬性,包含原型上的屬性。檢視for…in),基本就可以手工實現一個淺拷貝的程式碼了。
深拷貝:建立一個新的物件,將一個物件從記憶體中完整地拷貝出來一份給該新物件,並從堆記憶體中開闢一個全新的空間存放新物件,且新物件的修改並不會改變原物件,二者實現真正的分離。
看看現存的一些深拷貝的方法:
JSON.stringfy()
其實就是將一個 JavaScript 物件或值轉換為 JSON 字串,最後再用 JSON.parse()
的方法將JSON 字串生成一個新的物件。(點這瞭解:JSON.stringfy()、JSON.parse())
使用如下:
function deepClone(target) {
if (typeof target === 'object' && target !== null) {
return JSON.parse(JSON.stringify(target));
} else {
return target;
}
}
// 開頭的測試obj存在BigInt型別、迴圈參照,JSON.stringfy()執行會報錯,所以除去這兩個條件進行測試
const clonedObj = deepClone(obj)
// 測試
clonedObj === obj // false,返回的是一個新物件
clonedObj.arr === obj.arr // false,說明拷貝的不是參照
登入後複製
瀏覽器執行結果:
從以上結果我們可知JSON.stringfy()
存在以下一些問題:
執行會報錯:存在BigInt
型別、迴圈參照。
拷貝Date
參照型別會變成字串。
鍵值會消失:物件的值中為Function
、Undefined
、Symbol
這幾種型別,。
鍵值變成空物件:物件的值中為Map
、Set
、RegExp
這幾種型別。
無法拷貝:不可列舉屬性、物件的原型鏈。
補充:其他更詳細的內容請檢視官方檔案:JSON.stringify()
由於以上種種限制條件,JSON.stringfy()
方式僅限於深拷貝一些普通的物件,對於更復雜的資料型別,我們需要另尋他路。
手動遞迴實現深拷貝,我們只需要完成以下2點即可:
對於基礎型別,我們只需要簡單地賦值即可(使用=
)。
對於參照型別,我們需要建立新的物件,並通過遍歷鍵來賦值對應的值,這個過程中如果遇到 Object 型別還需要再次進行遍歷。
function deepClone(target) {
if (typeof target === 'object' && target) {
let cloneObj = {}
for (const key in target) { // 遍歷
const val = target[key]
if (typeof val === 'object' && val) {
cloneObj[key] = deepClone(val) // 是物件就再次呼叫該函數遞迴
} else {
cloneObj[key] = val // 基本型別的話直接複製值
}
}
return cloneObj
} else {
return target;
}
}
// 開頭的測試obj存在迴圈參照,除去這個條件進行測試
const clonedObj = deepClone(obj)
// 測試
clonedObj === obj // false,返回的是一個新物件
clonedObj.arr === obj.arr // false,說明拷貝的不是參照
登入後複製
瀏覽器執行結果:
該基礎版本存在許多問題:
不能處理迴圈參照。
只考慮了Object
物件,而Array
物件、Date
物件、RegExp
物件、Map
物件、Set
物件都變成了Object
物件,且值也不正確。
丟失了屬性名為Symbol
型別的屬性。
丟失了不可列舉的屬性。
原型上的屬性也被新增到拷貝的物件中了。
如果存在迴圈參照的話,以上程式碼會導致無限遞迴,從而使得堆疊溢位。如下例子:
const a = {}
const b = {}
a.b = b
b.a = a
deepClone(a)
登入後複製
物件 a 的鍵 b 指向物件 b,物件 b 的鍵 a 指向物件 a,檢視a
物件,可以看到是無限迴圈的:
對物件a
執行深拷貝,會出現死迴圈,從而耗盡記憶體,進而報錯:堆疊溢位
如何避免這種情況呢?一種簡單的方式就是把已新增的物件記錄下來,這樣下次碰到相同的物件參照時,直接指向記錄中的物件即可。要實現這個記錄功能,我們可以藉助 ES6 推出的 WeakMap
物件,該物件是一組鍵/值對的集合,其中的鍵是弱參照的。其鍵必須是物件,而值可以是任意的。(WeakMap
相關見這:WeakMap)
針對以上基礎版深拷貝存在的缺陷,我們進一步去完善,實現一個完美的深拷貝。
對於基礎版深拷貝存在的問題,我們一一改進:
存在的問題 | 改進方案 |
---|---|
1. 不能處理迴圈參照 | 使用 WeakMap 作為一個Hash表來進行查詢 |
2. 只考慮了Object 物件 | 當引數為 Date 、RegExp 、Function 、Map 、Set ,則直接生成一個新的範例返回 |
3. 屬性名為Symbol 的屬性4. 丟失了不可列舉的屬性 | 針對能夠遍歷物件的不可列舉屬性以及 Symbol 型別,我們可以使用 Reflect.ownKeys()注: Reflect.ownKeys(obj) 相當於[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)] |
4. 原型上的屬性 | Object.getOwnPropertyDescriptors()設定屬性描述物件,以及Object.create()方式繼承原型鏈 |
程式碼實現:
function deepClone(target) {
// WeakMap作為記錄物件Hash表(用於防止迴圈參照)
const map = new WeakMap()
// 判斷是否為object型別的輔助函數,減少重複程式碼
function isObject(target) {
return (typeof target === 'object' && target ) || typeof target === 'function'
}
function clone(data) {
// 基礎型別直接返回值
if (!isObject(data)) {
return data
}
// 日期或者正則物件則直接構造一個新的物件返回
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data)
}
// 處理常式物件
if (typeof data === 'function') {
return new Function('return ' + data.toString())()
}
// 如果該物件已存在,則直接返回該物件
const exist = map.get(data)
if (exist) {
return exist
}
// 處理Map物件
if (data instanceof Map) {
const result = new Map()
map.set(data, result)
data.forEach((val, key) => {
// 注意:map中的值為object的話也得深拷貝
if (isObject(val)) {
result.set(key, clone(val))
} else {
result.set(key, val)
}
})
return result
}
// 處理Set物件
if (data instanceof Set) {
const result = new Set()
map.set(data, result)
data.forEach(val => {
// 注意:set中的值為object的話也得深拷貝
if (isObject(val)) {
result.add(clone(val))
} else {
result.add(val)
}
})
return result
}
// 收集鍵名(考慮了以Symbol作為key以及不可列舉的屬性)
const keys = Reflect.ownKeys(data)
// 利用 Object 的 getOwnPropertyDescriptors 方法可以獲得物件的所有屬性以及對應的屬性描述
const allDesc = Object.getOwnPropertyDescriptors(data)
// 結合 Object 的 create 方法建立一個新物件,並繼承傳入原物件的原型鏈, 這裡得到的result是對data的淺拷貝
const result = Object.create(Object.getPrototypeOf(data), allDesc)
// 新物件加入到map中,進行記錄
map.set(data, result)
// Object.create()是淺拷貝,所以要判斷並遞迴執行深拷貝
keys.forEach(key => {
const val = data[key]
if (isObject(val)) {
// 屬性值為 物件型別 或 函數物件 的話也需要進行深拷貝
result[key] = clone(val)
} else {
result[key] = val
}
})
return result
}
return clone(target)
}
// 測試
const clonedObj = deepClone(obj)
clonedObj === obj // false,返回的是一個新物件
clonedObj.arr === obj.arr // false,說明拷貝的不是參照
clonedObj.func === obj.func // false,說明function也複製了一份
clonedObj.proto // proto,可以取到原型的屬性
登入後複製
詳細的說明見程式碼中的註釋,更多測試希望大家自己動手嘗試驗證一下以加深印象。
在遍歷 Object
型別資料時,我們需要把 Symbol
型別的鍵名也考慮進來,所以不能通過 Object.keys
獲取鍵名或 for...in
方式遍歷,而是通過Reflect.ownKeys()
獲取所有自身的鍵名(getOwnPropertyNames
和 getOwnPropertySymbols
函數將鍵名組合成陣列也行:[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]
),然後再遍歷遞迴,最終實現拷貝。
瀏覽器執行結果:
可以發現我們的cloneObj物件
和原來的obj物件
一模一樣,並且修改cloneObj物件
的各個屬性都不會對obj物件
造成影響。其他的大家再多嘗試體會哦!
【相關推薦:、】
以上就是一文帶你詳細瞭解JavaScript中的深拷貝的詳細內容,更多請關注TW511.COM其它相關文章!