先记住这个答案
WeakSet 弱关联可回收的成员,现代规则允许对象和非注册 Symbol,不支持任意字符串数字,也没有 size 或遍历成员的方法。它适合按对象身份记录是否已初始化、已校验等状态,同时避免标记集合本身额外保留对象。Set 则可保存任意值、枚举并计数,但会强引用对象成员。标记的业务范围必须明确:全程已访问与当前递归路径是不同集合,不能把共享子节点误判成循环。若需要重试失败任务,还应在成功后标记或显式撤销失败标记。
- 按对象身份标记,不按字段内容判重
- 弱成员不可枚举,不能用于完整历史报表
- 标记何时写入和删除取决于任务语义
同一个对象与相同内容的对象不同
示例的处理函数遇到同一个对象第二次返回 false,但遇到另一个相同 id 的对象仍会处理。这个行为适合实例级标记,不适合按接口记录编号去重。
这里的处理是同步且不会失败的计数,所以在标记后立刻完成。如果真实操作会抛错或异步失败,应设计开始、进行中和完成的状态,不能照搬一个布尔标记让失败对象永远无法重试。
const handled = new WeakSet();
let count = 0;
function handleOnce(record) {
if (handled.has(record)) return false;
handled.add(record);
count++;
return true;
}
const first = { id: 7 };
console.log(handleOnce(first));
console.log(handleOnce(first));
console.log(handleOnce({ id: 7 }));
console.log(count);查看输出与解释
true
false
true
2第二次传入相同引用被跳过,相同字段的新对象仍是另一实例。示例只验证身份标记,不把计数值当作 WeakSet 当前成员数,也不依赖垃圾回收事件。
图遍历要区分共享节点和真正的环
全程 visited 集合适合确保每个节点最多处理一次,共享子节点再次出现时可以跳过。当前递归路径集合则用于检测回边:进入节点时加入,离开时删除,只有再次遇到当前路径上的节点才表示环。
如果用永不删除的 WeakSet 检测所有循环,会把无环图中的共享节点也误报。集合类型本身不会决定算法语义,应先写清标记的有效范围,再选择 Set 或 WeakSet 是否符合持有需求。
需要列表或容量控制时使用显式集合
调试面板若要列出所有已处理对象,WeakSet 无法提供这种枚举;离线任务若要恢复处理历史,也不能把它当作持久状态。此类需求适合明确记录稳定标识,并定义清理期限。
现代 WeakSet 可接受非注册 Symbol,但旧引擎支持范围要另行核对,注册 Symbol 仍不适用。组件资源释放则应由生命周期显式执行,弱标记不负责自动调用销毁函数或取消订阅。
容易答错的地方
- 把 WeakSet 当作可遍历的弱数组
- 它只提供按已知成员查询和变更的能力,没有成员列表或可靠 size。需要展示、统计或逐个清理对象时,应使用能满足这些操作的结构,并承担明确的生命周期管理。
- 所有访问过的节点再次出现都判为循环
- 重复访问可能只是多个父节点共享同一个子节点,不一定形成回路。检测环应按当前路径或算法所需状态建模,正确处理进入与离开,不能让集合名称替代图关系推理。
面试官还会怎么问?
WeakSet 里能存普通数字标识吗?
不能,数字和普通字符串不属于允许的可回收成员类型。按业务 id 去重通常使用 Set;WeakSet 面向对象实例或符合规则的 Symbol,选择应匹配身份模型而不是只看内存偏好。
标记后处理失败怎么允许重试?
可以在成功后才写入完成标记,或在失败路径删除进行中标记,但并发任务还需要区分重复启动与完成状态。复杂状态更适合 WeakMap 保存明确阶段,避免一个布尔值承担多种含义。
用 Set 标记对象就一定内存泄漏吗?
不一定,若集合生命周期受控并及时清理,强引用可能正是需要的语义。泄漏来自不必要的长期保留,应根据是否需要枚举、计数和主动释放选择结构,不能把所有强集合都视为错误。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。