为何说2024最值得用数据结构是WeakMap?

前有科技后进阶 2024-05-27 05:44:19

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

1. 什么是 WeakMap

WeakMap 是一种键值对的集合,键必须是对象或非全局注册的 Symbol,这样可以保证唯一且不能被重新创建。而 WeakMap 的值可以是任意的 JavaScript 类型,并且不会创建对键的强引用。

WeakMap 是一个黑盒,没有键我们无法访问它的值

同时,WeakMap 对象是不可枚举的,无法获取集合的大小。

const sym1 = Symbol();const sym2 = Symbol("foo");const sym3 = Symbol("foo");// sym2 === sym3 返回 false

关于以上代码有两点需要说明:

Symbol("foo") 不会强制将字符串 “foo” 转换成 symbol 类型,而是每次都创建一个新的 symbolSymbol() 语法不会在整个代码库中创建一个可用的全局 symbol 类型。要创建跨文件可用的 symbol,甚至跨域(每个都有自己全局作用域)可以使用 Symbol.for() 、 Symbol.keyFor() 从全局 symbol 注册表设置和取得 symbol

一个对象作为 WeakMap 的键存在不会阻止该对象被垃圾回收。一旦一个对象作为键被回收,那么 WeakMap 中相应的值在没有其他引用的情况下便成为垃圾回收的候选对象。

总之,WeakMap 允许将数据与对象相关联,而不阻止键对象被垃圾回收,即使值本身引用了键。然而,WeakMap 无法获取键的生命周期,即无法枚举。如果 WeakMap 提供了任何获得其键列表的方法,那么列表将会依赖垃圾回收状态从而引入不确定性。因此,如果想获取键的列表,应该使用 Map 而不是 WeakMap。

2. 为什么需要 WeakMap

在 JavaScript 里,Map 可以通过两个数组实现,一个存放键,一个存放值。给 Map 设置值会同时将键和值添加到两个数组的末尾,从而使得键和值的索引在两个数组中存在。当从 Map 取值的时候,需要遍历所有的键,然后使用索引从存储值的数组中检索出相应的值。

但该实现会有两个很大的缺点:

首先赋值和搜索操作都是 O(n) 的时间复杂度(n 是键值对个数),因为这两个操作都需要遍历整个数组进行匹配。另外一个缺点是可能会导致内存泄漏,因为数组会一直引用着每个键和值。这种引用使得垃圾回收算法不能回收处理,即使没有其他任何引用存在。

相较之下,WeakMap 的键对象会强引用其值,直到该键对象被垃圾回收从而变为弱引用。因此,WeakMap:

不会阻止垃圾回收,直到垃圾回收器移除了键对象的引用任何值都可以被垃圾回收,只要键对象没有被 WeakMap 以外的地方引用

当将键映射到与键相关的信息,而该信息仅在键未被垃圾回收的情况下具有价值时,WeakMap 是一个特别有用的构造。

const wm1 = new WeakMap();const wm2 = new WeakMap();const wm3 = new WeakMap();const o1 = {};const o2 = function () {};const o3 = window;wm1.set(o1, 37);wm1.set(o2, "azerty");wm2.set(o1, o2);// 值可以是任何值,包括:对象、函数wm2.set(o2, undefined);wm2.set(wm1, wm2);// 键和值可以是任何类型,包括 WeakMaps!wm1.get(o2);// 输出 "azerty"wm2.get(o2);// undefined, because that is the set valuewm2.get(o3);// undefined, because there is no key for o3 on wm2wm1.has(o2); // truewm2.has(o2); // true (even if the value itself is 'undefined')wm2.has(o3); // falsewm3.set(o1, 37);wm3.get(o1); // 37wm1.has(o1); // truewm1.delete(o1);wm1.has(o1); // false3.WeakMap 的常见用例3.1 数据缓存

WeakMap 有助于数据存储,数据存储(memoization)是一种缓存昂贵计算结果并在接收到相同输入值时返回缓存结果的技术。

const memo = new WeakMap();// 创建一个占用很大内存的闭包function createLargeClosure() { const largeObj = { a: 1, b: 2, str: new Array(1000000).join('x'), // 很大的数据对象 }; const lc = function largeClosure() { return largeObj; }; return lc;}// 缓存数据的 memoize 方法function memoize(obj) { if (memo.has(obj)) { console.log('Get cached result'); return memo.get(obj); } const compute = obj.a + obj.b; console.log('Set computed result to caching map'); memo.set(obj, compute); return compute;}// 入口函数function start() { const lcObj = createLargeClosure(); // 返回一个函数 const timer = setInterval(() => { memoize(lcObj()); // 返回大对象 Object 作为 key }, 1000); // 清除定时器后不再持有对 lcObj 的引用可以回收内存 setTimeout(function () { clearInterval(timer); }, 5001);}3.2 自定义事件

某些项目可能需要自定义 Event 对象,如果只是将 Event 对象实现为单例,WeakMap 可以用于解决内存泄漏。 以下代码注册了一个以目标对象为 key 的事件处理程序并执行:

class EventEmitter { constructor() { // 使用 WeakMap this.targets = new WeakMap(); } // 添加事件 on(targetObject, handlers) { if (!this.targets.has(targetObject)) { this.targets.set(targetObject, {}); } const targetHandlers = this.targets.get(targetObject); Object.keys(handlers).forEach(handlerName => { targetHandlers[handlerName] = targetHandlers[handlerName] || []; targetHandlers[handlerName].push(handlers[handlerName].bind(targetObject)); }); } // 执行事件 fire(targetObject, handlerName, args) { const targetHandlers = this.targets.get(targetObject); if (targetHandlers && targetHandlers[handlerName]) { targetHandlers[handlerName].forEach(handler => handler(args)); } }}

下面代码调用 EventEmitter:

const emitter = new EventEmitter();function start() { const user = { name: 'John' }; const handlers = { sayHello: function() { console.log(`Hello, my name is ${this.name}`); }, sayGoodBye: function() { console.log(`Good bye, my name is ${this.name}`); } }; emitter.on(user, handlers); // 一般情况下第一个事件类型会是 string 类型,这里是 Object 对象类型 const timer = setInterval(() => { emitter.fire(user, 'sayHello'); emitter.fire(user, 'sayGoodBye'); }, 1000); setTimeout(function () { clearInterval(timer); }, 5001);}

当使用 WeakMap 后,调用 clearInterval 会清除对 user 对象的引用,此时当前 user 的所有事件处理程序都会被垃圾回收。

3.3 私有属性保护

开发者可以使用 WeakMap 关联对象实例与私有数据,从而获得以下好处:

与 Map 相比,WeakMap 不持有键对象的强引用,因此元数据与对象本身共享同样的生命周期,避免内存泄漏。与使用不可枚举对象和 / 或 Symbol 属性相比,WeakMap 位于对象外部,没有办法通过像 Object.getOwnPropertySymbols 等的反射方法来检索元数据。与闭包相比,构造函数可以复用同一个 WeakMap 对象来创建所有实例,从而节省内存,并且允许同一个类创建的不同实例读取彼此的私有成员。let Thing;{ const privateScope = new WeakMap(); let counter = 0; Thing = function () { this.someProperty = "foo"; // WeakMap 的 key 是对象实例 privateScope.set(this, { hidden: ++counter, }); }; Thing.prototype.showPublic = function () { return this.someProperty; }; Thing.prototype.showPrivate = function () { return privateScope.get(this).hidden; };}console.log(typeof privateScope);// 输出 "undefined"const thing = new Thing();console.log(thing);// 输出 Thing {someProperty: "foo"}thing.showPublic();// 输出 "foo"thing.showPrivate();// 输出 14. WeakMap 的局限性

尽管 WeakMap 有以上所述的诸多好处,但也有一些局限性:

键必须是对象:与 Map 不同,WeakMap 要求键是对象,而不是原始值。没有内置的迭代方法:WeakMap 没有用于迭代其条目的内置方法,例如 forEach 或 entries()。没有 clear 方法:没有内置方法可以清除整个 WeakMap。兼容性有限:一些较旧的浏览器不支持 WeakMap,从而限制了其兼容性。不过,Chrome>=36、Edge>=12、Safari>=8、Firefox>=6、Opera>=23、IE>=11 等以上版本都已经支持。5. 总结

WeakMap 为 JavaScript 中的高效内存管理和数据隐私提供了强大的功能和应用程序。 虽然有一些局限性,但它们的优点通常胜过这些缺点,使它们成为 JavaScript 开发人员工具包的宝贵补充。 随着语言的不断发展,未来可能会解决这些限制并进一步提高 WeakMap 在 JavaScript 应用程序中的实用性。

值得一提的是,除了使用 WeakMap 保持弱引用外,ECMAScript 2021 又引入了 WeakRef 功能,WeakRef 也可以保持对另一个对象的弱引用。WeakRef 不会阻止无法访问的对象的垃圾收集,当不想将对象永远保留在内存中时,它很有用。

因此,如果需要一个以项目中的对象作为标识符的数据结构,开发者可以考虑在使用常规对象或 Map 之前使用 WeakMap。

参考资料

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol

https://toastui.medium.com/lets-find-out-about-weakmap-2150905935d1

https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/464

https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap

https://caniuse.com/?search=WeakMap

https://dev.to/this-is-learning/unlocking-javascripts-secret-weapon-the-power-of-weakmaps-36ni

https://blog.techstackmedia.com/javascript-use-cases-of-weakmap

https://www.youtube.com/watch?v=sq35bQF3H34

0 阅读:38

前有科技后进阶

简介:感谢大家的关注