[JS] 深拷贝的实现

cnblogs 2024-07-17 08:11:00 阅读 83

这篇文章介绍了浅拷贝和深拷贝的区别,以及如何在JS中实现深拷贝,最后使用Jest进行测试。

浅拷贝和深拷贝的区别

  • 浅拷贝:浅拷贝指的是复制一个对象的时候,对于对象的一个属性,
    • 如果是基本数据类型,则复制其值;
    • 如果是引用数据类型,则复制其引用。
  • 深拷贝:深拷贝指的是复制一个对象的时候,对于对象的一个属性,
    • 如果是基本数据类型,则复制其值;
    • 如果是引用数据类型,则递归地深拷贝该对象。

从内存的堆区和栈区上观察它们的区别:

  • 浅拷贝:引用数据类型的属性值指向同一个对象实例:

    image-20240716165302305

  • 深拷贝:引用数据类型的属性值在深拷贝的时候会在堆区申请新的空间,然后填入新内容(递归调用):

    image-20240716165429589

如何实现深拷贝

接口定义:<code>deepClone(sourceObject): newObject

需要注意或者思考的问题:

  1. 小心循环引用导致的无限递归导致栈溢出;

    可以使用一个WeakMap记录已创建的对象,后续递归过程中如果引用了该对象,则直接填入其引用,而不是递归调用deepClone,从而避免了无限递归。

    其中WeakMap<source, target>用于记录源对象上的引用到目标对象上的引用的映射记录。因为在deepClone的过程中,我们是在使用DFS遍历source的过程中构建target

  2. 递归调用什么时候返回?

    • 基本数据类型直接返回;
    • 如果WeakMap上已经存在记录,说明存在循环引用,直接返回记录的引用,而不是递归调用。
  3. 如何获取对象上的key,常规的key、Symbol类型的key、不可遍历的key如何获取?

    直接使用Reflect.ownKeys可以获取对象上的常规key、Symbol-key和不可遍历的key。

  4. 拷贝对象的时候,对象属性的描述符也要复制;

    使用Reflect.getOwnPropertyDescriptor(target, key)方法可以获取target对象上key属性的描述符对象。

  5. 对于特殊类型的引用数据类型,应考虑对应的复制方法,如SetMap数据类型需要考虑添加记录的顺序

如何判断引用数据类型

可以使用typeof判断一个变量是否是object类型,使用typeof需要注意两个特例:

  1. typeof null === 'object'null是基本数据类型,但是会返回object
  2. 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

image-20240716175612883



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。