JavaScript完美深拷貝
阿新 • • 發佈:2022-04-07
JavaScript資料型別的拷貝原理
先看看JS資料型別圖(除了Object
,其他都是基礎型別):
在JavaScript中,基礎型別值的複製是直接拷貝一份新的一模一樣的資料,這兩份資料相互獨立,互不影響。而引用型別值(Object型別)的複製是傳遞物件的引用(也就是物件所在的記憶體地址,即指向物件的指標),相當於多個變數指向同一個物件,那麼只要其中的一個變數對這個物件進行修改,其他的變數所指向的物件也會跟著修改(因為它們指向的是同一個物件)。
評價一個深拷貝是否完善,請檢查以下問題是否都實現了:
-
基本型別資料
是否能拷貝? - 鍵和值都是基本型別的
普通物件
是否能拷貝? -
Symbol
-
Date
和RegExp
物件型別是否能拷貝? -
Map
和Set
物件型別是否能拷貝? -
Function
物件型別是否能拷貝?(函式我們一般不用深拷貝) - 物件的
原型
是否能拷貝? -
不可列舉屬性
是否能拷貝? -
迴圈引用
是否能拷貝?
怎樣?你寫的深拷貝夠完善嗎?
對於基礎版深拷貝存在的問題,我們一一改進:
存在的問題 | 改進方案 |
---|---|
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 } //處理Array物件 if(Array.isArray(data)){ let ary=[]; for(let i=0;i<data.length;i++){ ary.push(deepClone(data[i])); } return ary; } // 處理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) } 注意,我們直接使用=賦值不是淺拷貝,因為它是直接指向同一個物件了,並沒有返回一個新物件。 深淺拷貝主要針對的是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 // 測試 console.log(obj); const clonedObj = deepClone(obj) console.log(clonedObj ); clonedObj === obj // false,返回的是一個新物件 clonedObj.arr === obj.arr // false,說明拷貝的不是引用 clonedObj.func === obj.func // false,說明function也複製了一份 clonedObj.proto // proto,可以取到原型的屬性
可以發現我們的cloneObj物件
和原來的obj物件
一模一樣,並且修改cloneObj物件
的各個屬性都不會對obj物件
造成影響。
參考:https://blog.csdn.net/cc18868876837/article/details/114918262