跳到主要内容

深拷贝技巧与 WeakMap 的使用

在 JavaScript 开发中,我们经常需要对对象进行深拷贝操作,以避免对象之间的引用导致数据被意外修改。本文将介绍几种常用的深拷贝技巧,并重点讲解如何利用 WeakMap 解决深拷贝过程中的一些问题。

使用递归实现深拷贝

最基本的深拷贝方式是利用递归遍历对象的所有属性,逐一进行拷贝。下面是一个使用 ES5 语法实现的深拷贝函数:

function deepClone(origin, target) {
var tar = target || {};
var toStr = Object.prototype.toString;
var arrType = '[object Array]';

for (var key in origin) {
if (origin.hasOwnProperty(key)) {
var value = origin[key];

if (typeof value === 'object' && value !== null) {
tar[key] = toStr.call(value) === arrType ? [] : {};
deepClone(value, tar[key]);
} else {
tar[key] = value;
}
}
}

return tar;
}

这个函数通过判断属性值的类型,如果是对象就递归调用 deepClone 函数,否则直接赋值。需要注意的是,我们要先判断属性是否是对象自身的属性,避免原型链上的属性被拷贝。

利用构造函数实现深拷贝

另一种深拷贝的思路是利用对象的构造函数。我们可以通过判断属性值的类型,然后使用对应的构造函数创建一个新的对象或数组,再递归拷贝其中的属性。下面是示例代码

function deepClone(origin) {
if (origin == undefined || typeof origin !== 'object') {
return origin;
}

if (origin instanceof Date) {
return new Date(origin);
}

if (origin instanceof RegExp) {
return new RegExp(origin);
}

const target = new origin.constructor();

for (let key in origin) {
if (origin.hasOwnProperty(key)) {
target[key] = deepClone(origin[key]);
}
}

return target;
}

使用构造函数的好处是可以正确处理 Date、RegExp 等特殊类型的对象,同时代码也更加简洁。

解决循环引用问题

在对象存在循环引用的情况下,上面的深拷贝函数会出现死循环。例如:

let obj1 = {};
let obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;

为了解决这个问题,我们可以使用 WeakMap 来存储已拷贝过的对象,当再次遇到时直接返回,避免重复拷贝导致的死循环。改进后的深拷贝函数如下:

function deepClone(origin, hashMap = new WeakMap()) {
if (origin == undefined || typeof origin !== 'object') {
return origin;
}

if (origin instanceof Date) {
return new Date(origin);
}

if (origin instanceof RegExp) {
return new RegExp(origin);
}

const hashKey = hashMap.get(origin);

if (hashKey) {
return hashKey;
}

const target = new origin.constructor();
hashMap.set(origin, target);

for (let key in origin) {
if (origin.hasOwnProperty(key)) {
target[key] = deepClone(origin[key], hashMap);
}
}

return target;
}

WeakMap 的键是弱引用,当键名所引用的对象被垃圾回收时,对应的键值也会被移除。这个特性可以帮助我们避免内存泄漏。

WeakMap 与 Map 的区别

WeakMap 与 Map 有以下几点区别:

  1. Map 的键可以是任意类型,而 WeakMap 的键只能是对象
  2. WeakMap 的键名所引用的对象是弱引用,不会影响垃圾回收机制

正是由于 WeakMap 的这些特性,我们可以用它来解决一些内存管理方面的问题。

使用 WeakMap 管理事件监听器

在浏览器开发中,我们经常需要给 DOM 元素绑定事件监听器。但是当该元素被移除时,我们还需要手动移除事件监听器,否则会导致内存泄漏:

const btn = document.querySelector('#btn');
btn.addEventListener('click', handleBtnClick, false);

function handleBtnClick() {}

// 移除DOM元素时,需要手动解绑事件
btn.remove();
handleBtnClick = null;

利用 WeakMap,我们可以自动清理这些事件监听器:

const btn = document.querySelector('#btn');
const listenerMap = new WeakMap();

listenerMap.set(btn, handleBtnClick);
btn.addEventListener('click', listenerMap.get(btn), false);

function handleBtnClick() {}

// 移除DOM元素时,WeakMap中对应的键值会自动被垃圾回收
btn.remove();