- A+
所属分类:Web前端
浅拷贝和深拷贝的区别
- 浅拷贝:浅拷贝指的是复制一个对象的时候,对于对象的一个属性,
- 如果是基本数据类型,则复制其值;
- 如果是引用数据类型,则复制其引用。
- 深拷贝:深拷贝指的是复制一个对象的时候,对于对象的一个属性,
- 如果是基本数据类型,则复制其值;
- 如果是引用数据类型,则递归地深拷贝该对象。
从内存的堆区和栈区上观察它们的区别:
-
浅拷贝:引用数据类型的属性值指向同一个对象实例:
-
深拷贝:引用数据类型的属性值在深拷贝的时候会在堆区申请新的空间,然后填入新内容(递归调用):
如何实现深拷贝
接口定义: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