ES6 WeakMap 的实际用途是什么?

IT技术 javascript ecmascript-6 weakmap
2021-02-10 22:13:38

WeakMapECMAScript 6 中引入数据结构的实际用途是什么?

由于弱映射的键创建了对其对应值的强引用,确保插入弱映射的值只要其键还活着永远不会消失,因此不能用于备忘录表,缓存或其他任何您通常会使用弱引用、具有弱值的映射等的东西。

在我看来,这是:

weakmap.set(key, value);

...只是一种迂回的说法:

key.value = value;

我缺少哪些具体用例?

6个回答

从根本上说

WeakMaps 提供了一种从外部扩展对象而不干扰垃圾收集的方法。每当你想扩展一个对象但不能因为它是密封的 - 或者来自外部源 - 可以应用 Wea​​kMap。

WeakMap 是一个映射(字典),其中是弱的——也就是说,如果对键的所有引用都丢失了并且没有更多对值的引用——该可以被垃圾收集。我们先通过例子来说明这一点,然后稍微解释一下,最后以实际使用结束。

假设我使用的 API 为我提供了某个对象:

var obj = getObjectFromLibrary();

现在,我有一个使用该对象的方法:

function useObj(obj){
   doSomethingWith(obj);
}

我想跟踪某个对象调用该方法的次数,并报告它发生的次数是否超过 N 次。天真的人会认为使用 Map:

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);
}

这有效,但它有内存泄漏 - 我们现在跟踪传递给函数的每个库对象,以防止库对象被垃圾收集。相反 - 我们可以使用WeakMap

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);
}

内存泄漏消失了。

用例

一些会导致内存泄漏并由WeakMaps启用的用例包括:

  • 保留有关特定对象的私有数据,并且只允许引用 Map 的人访问它。私人符号提案将提供一种更临时的方法,但距离现在还有很长一段时间。
  • 保留有关库对象的数据而不更改它们或产生开销。
  • 保留关于一小组对象的数据,其中存在许多该类型的对象,以免导致 JS 引擎用于相同类型对象的隐藏类出现问题。
  • 在浏览器中保存有关主机对象(如 DOM 节点)的数据。
  • 从外部向对象添加功能(如另一个答案中的事件发射器示例)。

来看看实际使用

它可用于从外部扩展对象。让我们从 Node.js 的真实世界中给出一个实际的(改编的,有点真实 - 说明一点)示例。

比方说,你的Node.js,你有Promise对象-现在你想跟踪所有当前被拒绝Promise-然而,你希望让他们被的情况下,垃圾收集没有引用存在于他们。

现在,你希望将属性添加到显而易见的原因,本地对象-这样你就完蛋了。如果您保留对Promise的引用,则会导致内存泄漏,因为不会发生垃圾收集。如果您不保留引用,则无法保存有关个人Promise的其他信息。任何涉及保存Promise ID 的方案本质上都意味着您需要对它的引用。

输入弱图

WeakMaps 意味着很弱。没有办法枚举弱映射或获取其所有值。在弱映射中,您可以根据键存储数据,当键被垃圾收集时,值也会被收集。

这意味着给定一个Promise,您可以存储有关它的状态 - 并且该对象仍然可以被垃圾收集。稍后,如果您获得对对象的引用,您可以检查是否有任何与它相关的状态并报告它。

这被Petka Antonov用来实现未处理的拒绝钩子,如下所示

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
});

我们将有关Promise的信息保存在地图中,并且可以知道何时处理了被拒绝的Promise。

关于“让我们看看一个真正的用途......在地图中保留有关Promise的信息”,我不明白为什么promise_to_exception_Map需要弱:为什么在触发“rejectionHandled”后不手动删除地图中的条目?这是一个更好的方法。
2021-03-15 22:13:38
@Benjamin,我们需要区分对内存敏感缓存的需求和对 data_object 元组的需求。不要将这两个单独的要求混为一谈。您的called示例最好使用jsfiddle.net/f2efbm7z编写,并且没有演示弱映射的使用。事实上,它可以用总共6种方式更好地编写,我将在下面列出。
2021-03-17 22:13:38
@ltamajs4 当然,在useObj使用 aMap而不是 a示例中WeakMap我们使用传入的对象作为映射键。该对象永远不会从映射中删除(因为我们不知道何时这样做),因此始终存在对它的引用,并且永远不会被垃圾收集。在 WeakMap 示例中,只要对对象的所有其他引用都消失了 - 可以从WeakMap. 如果您仍然不确定我的意思,请告诉我
2021-03-21 22:13:38
从根本上说,弱映射的目的是对内存敏感的缓存。虽然它可以用来从外部扩展对象,但这是一个不正当的糟糕黑客,绝对不是它的正确目的
2021-04-02 22:13:38
你好!你能告诉我示例代码的哪一部分导致内存泄漏吗?
2021-04-04 22:13:38

这个答案在现实世界中似乎有偏见且无法使用。请按原样阅读,不要将其视为实验以外的任何其他实际选择

一个用例可能是将它用作听众的字典,我有一个同事就是这样做的。这是非常有用的,因为任何听众都直接针对这种做事方式。再见listener.on

但是从更抽象的角度来看,WeakMap它对于非物质化访问基本上任何东西都特别强大,您不需要命名空间来隔离其成员,因为这种结构的性质已经暗示了它。我很确定你可以通过替换笨拙的冗余对象键来做一些重大的内存改进(即使解构对你有用)。


在阅读接下来的内容之前

我现在确实意识到我的强调并不是解决问题的最佳方法,正如Benjamin Gruenbaum指出的那样(看看他的回答,如果它不在我的上面:p),这个问题不可能用普通的来解决Map,因为它会泄漏,因此主要优点WeakMap是它不会干扰垃圾收集,因为它们不保留引用。


这是我同事的实际代码(感谢的分享)

完整的源代码在这里,它是关于我上面谈到的监听器管理(你也可以看看规范

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);
    }
}
@axelduch,是的,那里也有参考资料。
2021-03-22 22:13:38
@Pacerier 更新了答案,感谢反馈
2021-03-23 22:13:38
@axelduch,哇,这个监听器处理的神话一直被兜售到 Javascript 社区,获得了 40 个赞!要了解为什么这个答案是完全错误的,请参阅stackoverflow.com/a/156618/632951下的评论
2021-03-28 22:13:38
双缓冲事件侦听器在其他语言中可能很快,但在这种情况下,它只是深奥而缓慢。那是我的三分钱。
2021-04-01 22:13:38
我不太明白你会如何使用它。当不再被引用时,它会导致 observable 与绑定到它的事件一起崩溃。我倾向于遇到的问题是不再引用观察者。我认为这里的解决方案只解决了一半的问题。我认为您无法使用 Wea​​kMap 解决观察者问题,因为它不可迭代。
2021-04-07 22:13:38

WeakMap 适用于封装和信息隐藏

WeakMap仅适用于 ES6 及更高版本。AWeakMap是键值对的集合,其中键必须是对象。在下面的例子中,我们WeakMap用两个项目构建一个

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

我们使用该set()方法来定义一个对象和另一个项目(在我们的例子中是一个字符串)之间的关联。我们使用该get()方法来检索与对象关联的项目。WeakMaps的有趣之处在于它持有对地图内键的弱引用。弱引用意味着如果对象被销毁,垃圾收集器将从 中删除整个条目WeakMap,从而释放内存。

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;
    }());
})()
重新“弱图适用于封装和信息隐藏”。仅仅因为你可以,并不意味着你应该。甚至在weakmap 被发明之前,Javascript 就有进行封装和信息隐藏的默认方式。到目前为止,实际上有6 种方法可以做到使用weakmap进行封装是一个丑陋的面子。
2021-04-09 22:13:38

𝗠𝗲𝘁𝗮𝗱𝗮𝘁𝗮

Weak Maps 可用于存储有关 DOM 元素的元数据,而不会干扰垃圾收集或让同事对您的代码感到生气。例如,您可以使用它们对网页中的所有元素进行数字索引。

𝗪𝗶𝘁𝗵𝗼𝘂𝘁 𝗪𝗲𝗮𝗸𝗠𝗮𝗽𝘀

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[(i * i) % 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"
  )   // }
);

𝗨𝘀𝗶𝗻𝗴 𝗪𝗲𝗮𝗸𝗠𝗮𝗽𝘀 𝗮𝗻𝗱 𝗪𝗲𝗮𝗸𝗦𝗲𝘁

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 well 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[(i * i) % 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 元素被垃圾收集。(i * i) % len可能看起来像一个没人会使用的古怪,但再想一想:大量的生产代码都有 DOM 引用,这些引用会在整个文档中反弹。现在,对于第二段代码,因为对元素的所有引用都很弱,当您删除一个节点时,浏览器能够确定该节点未被使用(您的代码无法访问),并且因此将其从内存中删除。您应该关注内存使用情况和内存锚点(例如未使用元素保存在内存中的第一个代码片段)的原因是因为更多的内存使用量意味着更多的浏览器 GC 尝试(尝试释放内存以避免浏览器崩溃)意味着较慢的浏览体验,有时会导致浏览器崩溃。

至于这些的 polyfill,我会推荐我自己的库(在这里找到 @github)。它是一个非常轻量级的库,可以简单地对其进行 polyfill,而无需您可能在其他 polyfill 中找到的任何过于复杂的框架。

~快乐编码!

@Pacerier感谢您的热心反馈,但是设置elements为null将不会允许浏览器GC在第一个片段形势的元素。这是因为您在元素上设置了自定义属性,然后仍然可以获取这些元素,并且仍然可以访问它们的自定义属性,从而防止它们中的任何一个被 GC 处理。把它想象成一串金属环。只要你能接触到链条中的至少一个环节,你就可以抓住链条中的那个环节,从而防止整个项目链坠入深渊。
2021-03-25 22:13:38
带有名为 vars 的 dunder 的生产代码让我呕吐
2021-03-26 22:13:38
@LukaszMatysiak这里有一个更短,更跨浏览器的版本:""+true我不会对代码进行这种更改,因为代码的目标是人类可读,而不是最大限度地节省空间。不是每个人都像你我一样了解 JS。有些初学者只是想开始使用这门语言。当我们炫耀我们对 JS 的高级知识时,这对他们没有一点帮助。
2021-03-29 22:13:38
@lolzery,Re“这可以防止 DOM 元素被垃圾收集”,您只需要设置elements为 null就可以了:它将被垃圾回收。& Re “在整个文档中反弹的 DOM 引用”,根本没有关系:一旦主链接elements消失,所有循环引用都将被 GC 处理。如果您的元素持有对它不需要的元素的引用,则修复代码并将引用设置为 null,当您使用它时。它将被GCed。 不需要弱图
2021-04-05 22:13:38
感谢您的清晰解释。一个例子比任何词都更有value。
2021-04-09 22:13:38

WeakMap用于缓存以不可变对象作为参数的函数的无忧记忆。

Memoization 是一种奇特的说法,即“在计算完值后,将其缓存起来,这样您就不必再次计算它了”。

下面是一个例子:

需要注意的几点:

  • Immutable.js 对象在修改它们时返回新对象(带有新指针),因此将它们用作 WeakMap 中的键可以保证相同的计算值。
  • WeakMap 非常适合备忘录,因为一旦对象(用作键)被垃圾回收,WeakMap 上的计算值也会被回收。
只要记忆化缓存是内存敏感的,而不是在整个 obj/function 生命周期中持续存在,这就是weakmap 的有效使用如果“记忆缓存”旨在在整个 obj/function 生命周期中保持持久,那么weakmap 是错误的选择:改用 6 种默认 javascript 封装技术中的任何一种
2021-04-01 22:13:38