返回首页

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 不再被引用时,数据自动清理

面试要点

  1. 弱引用概念:不阻止垃圾回收,对象只有弱引用时可以被回收
  2. 键的限制:WeakSet 只能存储对象,WeakMap 的键只能是对象
  3. 不可遍历:没有 size、forEach、keys/values/entries 方法
  4. 自动清理:当对象被回收后,WeakSet/WeakMap 中的条目自动消失
  5. 适用场景: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