27. 说说你对WeakSet和WeakMap的理解?
问题解析
WeakSet 和 WeakMap 是 ES6 引入的两种特殊的集合类型,它们是 Set 和 Map 的"弱引用"版本。面试中主要考察它们与普通 Set/Map 的区别、弱引用的概念、垃圾回收机制的影响以及适用场景。
核心概念
1. 弱引用(Weak Reference)
弱引用是指不会阻止垃圾回收的引用。当对象只有弱引用指向它时,垃圾回收器可以在任何时候回收该对象。
// 强引用:阻止垃圾回收
let obj = { data: 'important' };
// 只要 obj 存在,对象就不会被回收
// 弱引用:不阻止垃圾回收
// WeakMap 和 WeakSet 中的引用都是弱引用
2. WeakSet
WeakSet 是一个只能存储对象的集合,且对对象的引用是弱引用。
const weakSet = new WeakSet();
let obj1 = { name: 'Alice' };
let obj2 = { name: 'Bob' };
weakSet.add(obj1);
weakSet.add(obj2);
console.log(weakSet.has(obj1)); // true
obj1 = null; // 取消强引用
// obj1 指向的对象可能会被垃圾回收,weakSet 中自动移除
3. WeakMap
WeakMap 是一个键值对集合,键必须是对象,且对键的引用是弱引用。
const weakMap = new WeakMap();
let key = { id: 1 };
weakMap.set(key, 'value');
console.log(weakMap.get(key)); // 'value'
key = null; // 取消强引用
// key 指向的对象可能会被垃圾回收,weakMap 中对应的键值对自动移除
详细解答
1. WeakSet 的特点和限制
const weakSet = new WeakSet();
// ✅ 可以添加对象
weakSet.add({ name: 'Alice' });
// ❌ 不能添加原始值
// weakSet.add(1); // TypeError: Invalid value used in weak set
// weakSet.add('string'); // TypeError
// weakSet.add(null); // TypeError
// 主要方法
let obj = { data: 'test' };
weakSet.add(obj);
console.log(weakSet.has(obj)); // true
weakSet.delete(obj);
console.log(weakSet.has(obj)); // false
// ❌ 没有以下方法:
// - size 属性
// - forEach 方法
// - keys/values/entries 方法
// - 不能遍历
// ❌ 不能遍历
// for (const item of weakSet) {} // TypeError: weakSet is not iterable
2. WeakMap 的特点和限制
const weakMap = new WeakMap();
// ✅ 键必须是对象
let keyObj = { id: 1 };
weakMap.set(keyObj, 'value');
// ❌ 键不能是原始值
// weakMap.set('key', 'value'); // TypeError: Invalid value used as weak map key
// weakMap.set(123, 'value'); // TypeError
// 主要方法
console.log(weakMap.get(keyObj)); // 'value'
console.log(weakMap.has(keyObj)); // true
weakMap.delete(keyObj);
console.log(weakMap.has(keyObj)); // false
// ❌ 没有以下方法:
// - size 属性
// - clear 方法(早期版本有,后来移除)
// - forEach 方法
// - keys/values/entries 方法
// - 不能遍历
// ❌ 不能遍历
// for (const [key, value] of weakMap) {} // TypeError
3. 与普通 Set/Map 的对比
| 特性 | Set/Map | WeakSet/WeakMap |
|---|---|---|
| 存储类型 | Set: 任意值Map: 任意键值 | WeakSet: 仅对象WeakMap: 键仅对象 |
| 引用类型 | 强引用 | 弱引用 |
| size 属性 | 有 | 无 |
| 遍历 | 支持 | 不支持 |
| clear 方法 | 有 | 无(早期有,已移除) |
| forEach | 支持 | 不支持 |
| 垃圾回收 | 阻止回收 | 不阻止回收 |
深入理解
1. 垃圾回收机制
// 演示弱引用的垃圾回收特性
let obj = { data: 'sensitive' };
const weakMap = new WeakMap();
weakMap.set(obj, 'metadata');
// 此时 obj 有两个引用:
// 1. 变量 obj(强引用)
// 2. weakMap 中的键(弱引用)
obj = null; // 移除强引用
// 现在只有 weakMap 持有弱引用
// 垃圾回收器可以在任何时候回收该对象
// 回收后,weakMap 中对应的条目自动消失
// 注意:无法确定何时被回收,因为:
// 1. 无法遍历 WeakMap
// 2. 无法获取 size
// 3. 垃圾回收是不可预测的
2. 为什么不能有 size 和遍历方法?
// 假设 WeakMap 支持 size
const weakMap = new WeakMap();
let key = { id: 1 };
weakMap.set(key, 'value');
console.log(weakMap.size); // 假设输出 1
key = null;
// 垃圾回收可能在任何时候发生
// 如果此时访问 size,结果是不确定的
// 这会导致程序行为不可预测
// 同理,遍历也是不可能的
// 因为遍历过程中,元素可能被垃圾回收
3. 实际内存管理示例
// 模拟大量 DOM 元素的数据管理
function createDOMCache() {
const cache = new WeakMap();
return {
setData(element, data) {
cache.set(element, data);
},
getData(element) {
return cache.get(element);
},
hasData(element) {
return cache.has(element);
}
};
}
const domCache = createDOMCache();
// 创建大量 DOM 元素
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.id = `item-${i}`;
domCache.setData(div, {
index: i,
loaded: Date.now(),
data: new Array(1000).fill(i) // 大量数据
});
document.body.appendChild(div);
}
// 当 DOM 元素被移除时
const oldDiv = document.getElementById('item-0');
oldDiv.remove(); // 从 DOM 中移除
// oldDiv 的引用如果不再存在,垃圾回收器可以回收:
// 1. DOM 元素本身
// 2. WeakMap 中关联的数据
// 这防止了内存泄漏
最佳实践
1. WeakSet 的应用场景
// 1. 标记对象状态(不阻止回收)
const processingSet = new WeakSet();
function processObject(obj) {
if (processingSet.has(obj)) {
console.log('Already processing');
return;
}
processingSet.add(obj);
// 异步处理
setTimeout(() => {
console.log('Processing:', obj);
processingSet.delete(obj);
}, 1000);
}
// 2. 防止重复处理
const visitedNodes = new WeakSet();
function traverseDOM(node, callback) {
if (visitedNodes.has(node)) return;
visitedNodes.add(node);
callback(node);
for (const child of node.children) {
traverseDOM(child, callback);
}
}
// 3. 对象标记(类似打标签)
const validatedObjects = new WeakSet();
function validate(obj) {
// 验证逻辑...
validatedObjects.add(obj);
return true;
}
function isValidated(obj) {
return validatedObjects.has(obj);
}
2. WeakMap 的应用场景
// 1. 私有属性实现(经典用法)
const privateData = new WeakMap();
class Person {
constructor(name, age) {
privateData.set(this, { name, age });
}
getName() {
return privateData.get(this).name;
}
getAge() {
return privateData.get(this).age;
}
setAge(age) {
privateData.get(this).age = age;
}
}
const person = new Person('Alice', 25);
console.log(person.getName()); // 'Alice'
// 无法从外部访问 privateData
// 2. DOM 元素数据缓存
const elementCache = new WeakMap();
function getElementData(element) {
if (!elementCache.has(element)) {
elementCache.set(element, {
clickCount: 0,
lastClicked: null,
handlers: []
});
}
return elementCache.get(element);
}
function trackClick(element) {
const data = getElementData(element);
data.clickCount++;
data.lastClicked = new Date();
}
// 3. 计算结果缓存(对象参数)
const computeCache = new WeakMap();
function expensiveCompute(obj) {
if (computeCache.has(obj)) {
return computeCache.get(obj);
}
// 模拟昂贵计算
const result = Object.keys(obj).reduce((sum, key) => {
return sum + obj[key] * 1000;
}, 0);
computeCache.set(obj, result);
return result;
}
// 4. 实例元数据存储
const metadata = new WeakMap();
function setMetadata(obj, key, value) {
if (!metadata.has(obj)) {
metadata.set(obj, {});
}
metadata.get(obj)[key] = value;
}
function getMetadata(obj, key) {
return metadata.get(obj)?.[key];
}
// 使用
const myObj = { id: 1 };
setMetadata(myObj, 'created', Date.now());
setMetadata(myObj, 'author', 'Alice');
3. 避免内存泄漏的模式
// ❌ 错误:使用 Map 存储 DOM 数据会导致内存泄漏
const badCache = new Map();
function badAttachData(element, data) {
badCache.set(element, data);
}
// 即使 element 从 DOM 移除,Map 仍然持有强引用
// 数据和元素都无法被回收
// ✅ 正确:使用 WeakMap
const goodCache = new WeakMap();
function goodAttachData(element, data) {
goodCache.set(element, data);
}
// 当 element 从 DOM 移除且没有其他引用时
// 垃圾回收器可以回收 element 和关联的数据
// ❌ 错误:使用普通对象存储大量临时数据
const tempData = {};
function processTemp(id, data) {
tempData[id] = data; // 即使不再需要,数据也会一直存在
}
// ✅ 正确:使用 WeakMap(如果键是对象)
const tempWeakData = new WeakMap();
function processTempBetter(keyObj, data) {
tempWeakData.set(keyObj, data);
}
// 当 keyObj 不再被引用时,数据自动清理
面试要点
- 弱引用概念:不阻止垃圾回收,对象只有弱引用时可以被回收
- 键的限制:WeakSet 只能存储对象,WeakMap 的键只能是对象
- 不可遍历:没有 size、forEach、keys/values/entries 方法
- 自动清理:当对象被回收后,WeakSet/WeakMap 中的条目自动消失
- 适用场景:DOM 数据缓存、私有属性、临时元数据存储
常见面试题
// 面试题 1:WeakMap 和 Map 的主要区别?
// 1. WeakMap 的键必须是对象,Map 可以是任意类型
// 2. WeakMap 是弱引用,不阻止垃圾回收
// 3. WeakMap 不能遍历,没有 size 属性
// 4. WeakMap 没有 clear 方法
// 面试题 2:为什么 WeakMap 没有 size 属性?
// 因为垃圾回收是不可预测的,size 的值会随时变化
// 这会导致程序行为不确定
// 面试题 3:实现一个私有属性
const createPrivate = () => {
const weakMap = new WeakMap();
return (obj) => {
if (!weakMap.has(obj)) {
weakMap.set(obj, {});
}
return weakMap.get(obj);
};
};
const privateField = createPrivate();
class MyClass {
constructor() {
privateField(this).secret = 'hidden';
}
getSecret() {
return privateField(this).secret;
}
}
// 面试题 4:WeakSet 的应用场景?
// 1. 标记对象是否被处理过
// 2. 防止循环引用
// 3. 对象状态标记(不阻止回收)
// 面试题 5:以下代码会内存泄漏吗?
const map = new Map();
const weakMap = new WeakMap();
(function() {
let obj = { data: 'test' };
map.set(obj, 'value'); // 不会泄漏,obj 在作用域内
weakMap.set(obj, 'value'); // 不会泄漏
})();
// Map 中的条目会一直存在(除非手动删除)
// WeakMap 中的条目会被自动清理
// 面试题 6:实现一个带缓存的计算函数
function createMemoizedCompute() {
const cache = new WeakMap();
return function(obj) {
if (cache.has(obj)) {
console.log('Cache hit');
return cache.get(obj);
}
console.log('Computing...');
const result = Object.values(obj).reduce((a, b) => a + b, 0);
cache.set(obj, result);
return result;
};
}
const compute = createMemoizedCompute();
const data = { a: 1, b: 2, c: 3 };
compute(data); // Computing... 6
compute(data); // Cache hit 6