[JS] 深拷貝的實現
淺拷貝和深拷貝的區別
- 淺拷貝:淺拷貝指的是復制一個對象的時候,對于對象的一個屬性,
- 如果是基本數據類型,則復制其值;
- 如果是引用數據類型,則復制其引用。
- 深拷貝:深拷貝指的是復制一個對象的時候,對于對象的一個屬性,
- 如果是基本數據類型,則復制其值;
- 如果是引用數據類型,則遞歸地深拷貝該對象。
從內存的堆區和棧區上觀察它們的區別:
-
淺拷貝:引用數據類型的屬性值指向同一個對象實例:

-
深拷貝:引用數據類型的屬性值在深拷貝的時候會在堆區申請新的空間,然后填入新內容(遞歸調用):

如何實現深拷貝
接口定義:deepClone(sourceObject): newObject
需要注意或者思考的問題:
-
小心循環引用導致的無限遞歸導致棧溢出;
可以使用一個WeakMap記錄已創建的對象,后續遞歸過程中如果引用了該對象,則直接填入其引用,而不是遞歸調用deepClone,從而避免了無限遞歸。
其中
WeakMap<source, target>用于記錄源對象上的引用到目標對象上的引用的映射記錄。因為在deepClone的過程中,我們是在使用DFS遍歷source的過程中構建target。 -
遞歸調用什么時候返回?
- 基本數據類型直接返回;
- 如果
WeakMap上已經存在記錄,說明存在循環引用,直接返回記錄的引用,而不是遞歸調用。
-
如何獲取對象上的key,常規的key、Symbol類型的key、不可遍歷的key如何獲取?
直接使用
Reflect.ownKeys可以獲取對象上的常規key、Symbol-key和不可遍歷的key。 -
拷貝對象的時候,對象屬性的描述符也要復制;
使用
Reflect.getOwnPropertyDescriptor(target, key)方法可以獲取target對象上key屬性的描述符對象。 -
對于特殊類型的引用數據類型,應考慮對應的復制方法,如
Set和Map數據類型需要考慮添加記錄的順序
如何判斷引用數據類型
可以使用typeof判斷一個變量是否是object類型,使用typeof需要注意兩個特例:
typeof null === 'object',null是基本數據類型,但是會返回object;typeof function(){} === 'function',function是引用數據類型且是object的子類型,但是會返回function。
function isObject(target){
return (typeof target==='object' && target!==null) || typeof target==='function';
}
代碼
function deepClone(target){
// 提前記錄clone的鍵值對,用于處理循環引用
const map = new WeakMap();
/**
* 輔助函數:判斷是否是對象類型
* 需要注意`null` 和 `function`
* @returns
*/
function isObject(target){
return (typeof target === 'object' && target !== null)
|| typeof target === 'function'
}
function clone(target){
/**
* 基本數據類型
* 操作:直接返回
*/
if(!isObject(target))return target;
/**
* Date和RegExp對象類型
* 操作:使用構造函數復制
*/
if([Date, RegExp].includes(target.constructor)){
return new target.constructor(target);
}
/**
* 函數類型
*/
if(typeof target==='function'){
return new Function('return ' + target.toString())();
}
/**
* 數組類型
*/
if(Array.isArray(target)){
return target.map(el => clone(el));
}
/**
* 檢查是否存在循環引用
*/
if(map.has(target))return map.get(target);
/**
* 處理Map對象類型
*/
if(target instanceof Map){
const res = new Map();
map.set(target, res);
target.forEach((val, key) => {
// 如果Map中的val是對象,也得深拷貝
res.set(key, isObject(val) ? clone(val) : val);
})
return res;
}
/**
* 處理Set對象類型
*/
if(target instanceof Set){
const res = new Set();
map.set(target, res);
target.forEach(val => {
// 如果val是對象類型,則遞歸深拷貝
res.add(isObject(val) ? clone(val) : val);
})
return res;
}
//==========================================
// 接下來是常規對象類型
//==========================================
// 收集key(包括Symbol和不可枚舉的屬性)
const keys = Reflect.ownKeys(target);
// 收集各個key的描述符
const allDesc = {};
keys.forEach(key => {
allDesc[key] = Reflect.getOwnPropertyDescriptor(target, key);
})
// 創建新對象(淺拷貝)
const res = Reflect.construct(Reflect.getPrototypeOf(target).constructor, []);
// 在遞歸調用clone之前記錄新對象,避免循環
map.set(target, res);
// 賦值并檢查是否val是否為對象類型
keys.forEach(key => {
// 添加對象描述符
Reflect.defineProperty(res, key, allDesc[key]);
// 賦值
const val = target[key];
res[key] = isObject(val) ? clone(val) : val;
});
return res;
}
return clone(target);
}
使用jest測試
安裝jest
pnpm install jest --save-dev
這里我使用的版本是:
{
...
"devDependencies": {
"jest": "^29.7.0"
},
...
}
指令
package.json
{
...
"scripts": {
"test": "jest"
},
...
}
編寫測試用例
deepClone.test.js
const deepClone = require('./deepClone');
test('deep clone primitive types', () => {
expect(deepClone(42)).toBe(42);
expect(deepClone('hello')).toBe('hello');
expect(deepClone(null)).toBeNull();
expect(deepClone(undefined)).toBeUndefined();
expect(deepClone(true)).toBe(true);
});
test('deep clone array', () => {
const arr = [1, { a: 2 }, [3, 4]];
const clonedArr = deepClone(arr);
expect(clonedArr).toEqual(arr);
expect(clonedArr).not.toBe(arr);
expect(clonedArr[1]).not.toBe(arr[1]);
expect(clonedArr[2]).not.toBe(arr[2]);
});
test('deep clone object', () => {
const obj = { a: 1, b: { c: 2 } };
const clonedObj = deepClone(obj);
expect(clonedObj).toEqual(obj);
expect(clonedObj).not.toBe(obj);
expect(clonedObj.b).not.toBe(obj.b);
});
test('deep clone Map', () => {
const map = new Map();
map.set('a', 1);
map.set('b', { c: 2 });
const clonedMap = deepClone(map);
expect(clonedMap).toEqual(map);
expect(clonedMap).not.toBe(map);
expect(clonedMap.get('b')).not.toBe(map.get('b'));
});
test('deep clone Set', () => {
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const set = new Set([1, 'string', obj1, obj2]);
const clonedSet = deepClone(set);
expect(clonedSet).toEqual(set);
expect(clonedSet).not.toBe(set);
expect(clonedSet.has(1)).toBe(true);
expect(clonedSet.has('string')).toBe(true);
const clonedObj1 = Array.from(clonedSet).find(item => typeof item === 'object' && item.a === 1);
const clonedObj2 = Array.from(clonedSet).find(item => typeof item === 'object' && item.b === 2);
expect(clonedObj1).toEqual(obj1);
expect(clonedObj1).not.toBe(obj1);
expect(clonedObj2).toEqual(obj2);
expect(clonedObj2).not.toBe(obj2);
});
test('deep clone with Symbol keys', () => {
const sym = Symbol('key');
const obj = { [sym]: 1, a: 2 };
const clonedObj = deepClone(obj);
expect(clonedObj).toEqual(obj);
expect(clonedObj[sym]).toBe(1);
expect(clonedObj.a).toBe(2);
});
test('deep clone with non-enumerable properties', () => {
const obj = {};
Object.defineProperty(obj, 'a', { value: 1, enumerable: false });
const clonedObj = deepClone(obj);
expect(clonedObj).toHaveProperty('a', 1);
expect(Object.keys(clonedObj)).not.toContain('a');
});
test('deep clone with property descriptors', () => {
const obj = {};
Object.defineProperty(obj, 'a', {
value: 1,
writable: false,
configurable: false,
enumerable: true
});
const clonedObj = deepClone(obj);
const desc = Object.getOwnPropertyDescriptor(clonedObj, 'a');
expect(desc.value).toBe(1);
expect(desc.writable).toBe(false);
expect(desc.configurable).toBe(false);
expect(desc.enumerable).toBe(true);
});
test('deep clone circular references', () => {
const obj = { a: 1 };
obj.self = obj;
const clonedObj = deepClone(obj);
expect(clonedObj).toEqual(obj);
expect(clonedObj.self).toBe(clonedObj);
expect(clonedObj.self).not.toBe(obj);
});
測試結果
npm run test


浙公網安備 33010602011771號