What are the actual uses of ES6 WeakMap?
ECMAScript 6中引入的
由于弱映射的键会对其对应的值建立强引用,因此确保只要弱键仍处于活动状态,插入到弱映射中的值就不会消失,因此不能将其用于备忘表, 缓存或其他通常使用弱引用,弱值映射等的内容。
在我看来,这是:
1 | weakmap.set(key, value); |
...只是一种回旋的说法:
1 | key.value = value; |
我缺少哪些具体用例?
从根本上
WeakMaps提供了一种从外部扩展对象而不干扰垃圾回收的方法。每当您想扩展对象但由于密封而不能扩展对象时(或者从外部源扩展)时,都可以应用WeakMap。
WeakMap是键弱的地图(词典),也就是说,如果丢失了对键的所有引用,并且不再有对该值的引用,则可以对该值进行垃圾回收。让我们首先通过示例展示它,然后进行一些解释,最后完成实际使用。
假设我使用的API给了我一个特定的对象:
1 | var obj = getObjectFromLibrary(); |
现在,我有一个使用该对象的方法:
1 2 3 | function useObj(obj){ doSomethingWith(obj); } |
我想跟踪用某个对象调用该方法的次数,并报告该方法是否发生了N次以上。天真的会想到使用Map:
1 2 3 4 5 6 7 8 | var map = new Map(); // maps can have object keys function useObj(obj){ doSomethingWith(obj); var called = map.get(obj) || 0; called++; // called one more time if(called > 10) report(); // Report called more than 10 times map.set(obj, called); } |
这可行,但是会发生内存泄漏-我们现在跟踪传递给该函数的每个库对象,从而避免垃圾回收。相反,我们可以使用
1 2 3 4 5 6 7 8 | var map = new WeakMap(); // create a weak map function useObj(obj){ doSomethingWith(obj); var called = map.get(obj) || 0; called++; // called one more time if(called > 10) report(); // Report called more than 10 times map.set(obj, called); } |
并且内存泄漏消失了。
用例
否则会导致内存泄漏并由
- 保留有关特定对象的私人数据,并且仅授予参考地图的人员访问。私有符号提案中出现了一种更临时的方法,但是距离现在还有很长时间。
- 保留有关库对象的数据,而无需更改它们或引起开销。
- 保留有关少量对象的数据(其中存在许多类型的对象)不会引起JS引擎用于相同类型的对象的隐藏类问题。
- 在浏览器中保留有关主机对象(如DOM节点)的数据。
- 从外部向对象添加功能(如另一个答案中的事件发射器示例)。
让我们看一下真正的用途
它可以用于从外部扩展对象。让我们从Node.js的真实世界中给出一个实用的(经过改编的,真实的)实例。
假设您是Node.js,并且有
现在,由于明显的原因,您不想将属性添加到本机对象中-因此您陷入了困境。如果保留对Promise的引用,则将导致内存泄漏,因为不会发生垃圾回收。如果您不保留引用,那么您将无法保存有关单个承诺的其他信息。任何涉及保存承诺ID的方案本质上都意味着您需要对其进行引用。
输入弱地图
WeakMaps表示键很弱。无法枚举弱映射或获取其所有值。在弱映射中,您可以基于密钥存储数据,并且在密钥被垃圾回收时也存储值。
这意味着只要有一个承诺,您就可以存储有关它的状态-而且该对象仍可以被垃圾回收。稍后,如果获得对对象的引用,则可以检查是否具有与该对象相关的任何状态并报告该状态。
Petka Antonov使用它来实现未处理的拒绝挂钩,如下所示:
1 2 3 4 | process.on('unhandledRejection', function(reason, p) { console.log("Unhandled Rejection at: Promise", p," reason:", reason); // application specific logging, throwing an error, or other logic here }); |
我们将有关诺言的信息保存在地图中,并且可以知道何时处理了被拒绝的诺言。
在现实世界中,这个答案似乎是有偏见且无法使用的。请按原样阅读,不要将其视为除实验以外的其他选择
一个用例可能是将其用作侦听器的字典,我有一个这样做的同事。这非常有用,因为任何听众都可以通过这种方式直接成为目标。再见
但是从更抽象的角度来看,
在阅读下一步之前
我现在确实意识到我的强调并不是解决问题的最佳方法,并且正如本杰明·格伦鲍姆(Benjamin Gruenbaum)所指出的(请查看他的回答,如果还没有超过我的:p),那么使用常规
这是我同事的实际代码(感谢他的分享)
这里有完整的源代码,是关于我在上面讨论的监听器管理的(您也可以查看规格)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | var listenableMap = new WeakMap(); export function getListenable (object) { if (!listenableMap.has(object)) { listenableMap.set(object, {}); } return listenableMap.get(object); } export function getListeners (object, identifier) { var listenable = getListenable(object); listenable[identifier] = listenable[identifier] || []; return listenable[identifier]; } export function on (object, identifier, listener) { var listeners = getListeners(object, identifier); listeners.push(listener); } export function removeListener (object, identifier, listener) { var listeners = getListeners(object, identifier); var index = listeners.indexOf(listener); if(index !== -1) { listeners.splice(index, 1); } } export function emit (object, identifier, ...args) { var listeners = getListeners(object, identifier); for (var listener of listeners) { listener.apply(object, args); } } |
1 2 3 4 5 6 | var map = new WeakMap(); var pavloHero = {first:"Pavlo", last:"Hero"}; var gabrielFranco = {first:"Gabriel", last:"Franco"}; map.set(pavloHero,"This is Hero"); map.set(gabrielFranco,"This is Franco"); console.log(map.get(pavloHero));//This is Hero |
我们使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | var TheatreSeats = (function() { var priv = new WeakMap(); var _ = function(instance) { return priv.get(instance); }; return (function() { function TheatreSeatsConstructor() { var privateMembers = { seats: [] }; priv.set(this, privateMembers); this.maxSize = 10; } TheatreSeatsConstructor.prototype.placePerson = function(person) { _(this).seats.push(person); }; TheatreSeatsConstructor.prototype.countOccupiedSeats = function() { return _(this).seats.length; }; TheatreSeatsConstructor.prototype.isSoldOut = function() { return _(this).seats.length >= this.maxSize; }; TheatreSeatsConstructor.prototype.countFreeSeats = function() { return this.maxSize - _(this).seats.length; }; return TheatreSeatsConstructor; }()); })() |
我将
记忆化是一种奇特的说法,即"在计算了值之后,对其进行缓存,这样就不必再次计算它"。
这是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // using immutable.js from here https://facebook.github.io/immutable-js/ const memo = new WeakMap(); let myObj = Immutable.Map({a: 5, b: 6}); function someLongComputeFunction (someImmutableObj) { // if we saved the value, then return it if (memo.has(someImmutableObj)) { console.log('used memo!'); return memo.get(someImmutableObj); } // else compute, set, and return const computedValue = someImmutableObj.get('a') + someImmutableObj.get('b'); memo.set(someImmutableObj, computedValue); console.log('computed value'); return computedValue; } someLongComputeFunction(myObj); someLongComputeFunction(myObj); someLongComputeFunction(myObj); // reassign myObj = Immutable.Map({a: 7, b: 8}); someLongComputeFunction(myObj); |
1 | <script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"> |
注意事项:
- Immutable.js对象在您对其进行修改时会返回新对象(带有新指针),因此在WeakMap中将它们用作键可确保相同的计算值。
- WeakMap非常适合用于备忘录,因为一旦对象(用作键)被垃圾回收,WeakMap上的计算值也会被收集。
????????????????
弱映射可用于存储有关DOM元素的元数据,而不会干扰垃圾回收或使同事生气于您的代码。例如,您可以使用它们对网页中的所有元素进行数字索引。
?????????????? ???????????????? ???? ????????????????:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | var elements = document.getElementsByTagName('*'), i = -1, len = elements.length; while (++i !== len) { // Production code written this poorly makes me want to cry: elements[i].lookupindex = i; elements[i].elementref = []; elements[i].elementref.push( elements[Math.pow(i, 2) % len] ); } // Then, you can access the lookupindex's // For those of you new to javascirpt, I hope the comments below help explain // how the ternary operator (?:) works like an inline if-statement document.write(document.body.lookupindex + '<br />' + ( (document.body.elementref.indexOf(document.currentScript) !== -1) ? // if(document.body.elementref.indexOf(document.currentScript) !== -1){ "true" : // } else { "false" ) // } ); |
?????????? ???????????????? ?????? ????????????????:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | var DOMref = new WeakMap(), __DOMref_value = Array, __DOMref_lookupindex = 0, __DOMref_otherelement = 1, elements = document.getElementsByTagName('*'), i = -1, len = elements.length, cur; while (++i !== len) { // Production code written this greatly makes me want to ??: cur = DOMref.get(elements[i]); if (cur === undefined) DOMref.set(elements[i], cur = new __DOMref_value) cur[__DOMref_lookupindex] = i; cur[__DOMref_otherelement] = new WeakSet(); cur[__DOMref_otherelement].add( elements[Math.pow(i, 2) % len] ); } // Then, you can access the lookupindex's cur = DOMref.get(document.body) document.write(cur[__DOMref_lookupindex] + '<br />' + ( cur[__DOMref_otherelement].has(document.currentScript) ? // if(cur[__DOMref_otherelement].has(document.currentScript)){ "true" : // } else { "false" ) // } ); |
?????? ????????????????????
除了弱映射版本更长的事实之外,这种差异看起来可以忽略不计,但是上面显示的两段代码之间存在很大的差异。在没有弱映射的第一段代码中,这段代码存储DOM元素之间的所有引用。这样可以防止DOM元素被垃圾回收。
至于用于这些的polyfill,我会推荐我自己的库(在@github上找到)。它是一个非常轻量级的库,可以简单地对其进行填充,而无需您在其他polyfill中找到任何过于复杂的框架。
?编码愉快!