ES6 WeakMap的实际用途是什么?


397

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

由于弱映射的键会强烈引用其对应的值,因此确保只要弱键仍处于活动状态,插入到弱映射中的值就不会消失,因此不能将其用于备忘表,缓存或其他通常使用弱引用,弱值映射等的内容。

在我看来,这是:

weakmap.set(key, value);

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

key.value = value;

我缺少哪些具体用例?




35
实际使用案例:存储DOM节点的自定义数据。
Felix Kling 2015年

您提到的所有弱引用用例也非常重要。由于它们引入了不确定性,因此将它们添加到语言中变得更加困难。马克·米勒(Mark Miller)和其他人在弱引用方面做了很多工作,我认为它们最终会来的。最终
本杰明·格伦鲍姆

2
WeakMaps可用于检测内存泄漏:stevehanov.ca/blog/?id=148
theWebalyst,

Answers:


513

从根本上

WeakMaps提供了一种从外部扩展对象而不干扰垃圾回收的方法。每当您想扩展对象但由于密封而无法扩展对象时(或从外部来源扩展)时,都可以应用WeakMap。

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

并且内存泄漏消失了。

用例

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

  • 保留有关特定对象的私人数据,并且仅授予参考地图的人员访问。私有符号提案中出现了一种更临时的方法,但是距离现在还有很长时间。
  • 保留有关库对象的数据,而无需更改它们或引起开销。
  • 保留有关少量对象的数据(其中存在许多类型的对象)不会引起JS引擎用于相同类型的对象的隐藏类的问题。
  • 在浏览器中保留有关主机对象(如DOM节点)的数据。
  • 从外部向对象添加功能(如另一个答案中的事件发射器示例)。

让我们看一下真正的用途

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

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

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

输入弱地图

WeakMaps表示很弱。无法枚举弱映射或获取其所有值。在弱映射中,您可以基于密钥存储数据,并且在密钥被垃圾回收时也存储值。

这意味着只要有一个承诺,您就可以存储有关它的状态-并且该对象仍可以被垃圾回收。稍后,如果获得对对象的引用,则可以检查是否具有与该对象相关的任何状态并报告该状态。

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

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


8
你好!您能否告诉我示例代码的哪一部分会导致内存泄漏?
ltamajs

15
@ ltamajs4可以肯定,在useObj示例中使用a Map而不是a,WeakMap我们将传入的对象用作映射键。该对象永远不会从地图上删除(因为我们不知道何时执行此操作),因此始终有对其的引用,并且永远也不会对其进行垃圾回收。在WeakMap示例中,一旦对该对象的所有其他引用都消失了,则可以从中清除该对象WeakMap。如果您仍然不确定我的意思,请告诉我
Benjamin Gruenbaum

@Benjamin,我们需要区分对内存敏感的缓存的需要和对data_object元组的需要。不要混淆这两个单独的要求。您的called示例最好使用jsfiddle.net/f2efbm7z编写,并且不会演示使用弱映射。实际上,最好用总共6种方式编写,下面将列出。
Pacerier

从根本上说,弱映射的目的是对内存敏感的缓存。虽然它可以用来从外部扩展对象,但这是不受欢迎的烂招,绝对不是其适当的用途
Pacerier's

1
如果要保留承诺和已处理/拒绝的次数之间的联系,请使用1)符号;p[key_symbol] = data。或2)独特的命名;p.__key = data。或3)私人范围;(()=>{let data; p.Key = _=>data=_;})()。或4)用1或2或3代理。或5)用1或2或3 替代/扩展Promise类。6)用所需成员的元组替换/扩展Promise类。—无论如何,除非需要内存敏感的缓存否则不需要弱映射
Pacerier

48

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

一个用例可能是将其用作侦听器的字典,我有一个同事做到这一点。这非常有用,因为任何听众都可以通过这种方式直接成为目标。再见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);
    }
}

2
我不太了解您将如何使用它。当不再引用该可观察对象时,它将导致与其绑定的事件一起崩溃。我倾向于遇到的问题是不再引用观察者时。我认为这里的解决方案只能解决一半的问题。我认为您无法使用WeakMap解决观察者问题,因为它不是可迭代的。
jgmjgm

1
在其他语言中,双缓冲事件侦听器可能很快,但是在这种情况下,它只是简单而深奥的。那是我的三分钱。
杰克·吉芬

@axelduch,哇,这个听众句柄的神话一直被贩卖到Javascript社区,获得40票赞成!要了解为什么这个答案是完全错误的,请参阅stackoverflow.com/a/156618/632951
Pacerier

1
@Pacerier更新了答案,感谢您的反馈
axelduch

1
@axelduch,是的,那里也有裁判。
Pacerier

18

WeakMap 适用于封装和信息隐藏

WeakMap仅适用于ES6及更高版本。A WeakMap是键和值对的集合,其中键必须是对象。在以下示例中,我们构建了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;
    }());
})()

4
关于“弱映射在封装和信息隐藏方面效果很好”。仅仅因为您可以,并不意味着您应该这样做。Javascript具有默认的封装和隐藏信息的方式,甚至在弱映射被发明之前。到目前为止,实际上有6种方法可以做到这一点。使用weakmap进行封装是一个丑陋的facepalm。
Pacerier

12

𝗠𝗲𝘁𝗮𝗱𝗮𝘁𝗮

弱映射可用于存储有关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 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[(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中找到任何过于复杂的框架。

〜编码愉快!


1
感谢您的明确解释。一个例子比什么都重要。
newguy

@lolzery,Re“ 这可以防止DOM元素被垃圾回收 ”,您所需要做的就是将其设置elements为null并完成操作:将它进行GCed。 Re“ 在整个文档中反弹的DOM引用 ”完全没有关系:一旦主链接elements消失,所有循环引用都将被GC。如果您的元素持有对不需要的元素的引用,请修复代码,并在使用完后将ref设置为null。将被GC化。 不需要弱映射
Pacerier

2
@Pacerier感谢您的热心反馈,但是设置elements为null将不会允许浏览器GC在第一个片段形势的元素。这是因为您在元素上设置了自定义属性,然后仍可以获取那些元素,并且仍可以访问其自定义属性,从而防止对其中的任何元素进行GC处理。可以将其视为一串金属环。只要您可以访问链中的至少一个链接,就可以抓住链中的该链接,从而防止整个物品链掉入深渊。
杰克·吉芬

1
dunder名为vars的生产代码让我呕吐
Barbu Barbu

10

我使用WeakMap将不可变对象作为其参数的函数的无忧备忘录的缓存。

记忆化是一种奇特的说法,即“在计算了值之后,对其进行缓存,这样就不必再次计算”。

这是一个例子:

注意事项:

  • Immutable.js对象在您对其进行修改时会返回新对象(带有新的指针),因此在WeakMap中将它们用作键可确保相同的计算值。
  • WeakMap非常适合用于备忘录,因为一旦对象(用作键)被垃圾回收,WeakMap上的计算值也会被回收。

1
只要备忘录高速缓存对内存敏感,并且在obj / function的整个生命周期中都不持久,则这是弱映射的有效用法。如果“内存缓存”在obj / function的整个生命周期中都是持久的,那么weakmap是错误的选择:改用 6种默认javascript封装技术中的任何一种
Pacerier

3

我有一个基于简单功能的用例/ WeakMaps示例。

管理一批用户

我开始了与一个User其属性包括一个对象fullnameusernameagegender和被调用的方法print,其打印的其他属性的人类可读摘要。

/**
Basic User Object with common properties.
*/
function User(username, fullname, age, gender) {
    this.username = username;
    this.fullname = fullname;
    this.age = age;
    this.gender = gender;
    this.print = () => console.log(`${this.fullname} is a ${age} year old ${gender}`);
}

然后,我添加了一个Map,users以保留由键入的多个用户的集合username

/**
Collection of Users, keyed by username.
*/
var users = new Map();

添加集合还需要帮助程序功能来添加,获取,删除用户,甚至为了完整起见还需要打印所有用户的功能。

/**
Creates an User Object and adds it to the users Collection.
*/
var addUser = (username, fullname, age, gender) => {
    let an_user = new User(username, fullname, age, gender);
    users.set(username, an_user);
}

/**
Returns an User Object associated with the given username in the Collection.
*/
var getUser = (username) => {
    return users.get(username);
}

/**
Deletes an User Object associated with the given username in the Collection.
*/
var deleteUser = (username) => {
    users.delete(username);
}

/**
Prints summary of all the User Objects in the Collection.
*/
var printUsers = () => {
    users.forEach((user) => {
        user.print();
    });
}

在运行上述所有代码(例如NodeJS)的过程中,只有usersMap引用了整个过程中的User Objects。没有其他参考单个用户对象。

作为示例,我在交互式NodeJS shell上运行此代码,我添加了四个用户并打印它们: 添加和打印用户

在不修改现有代码的情况下向用户添加更多信息

现在说一个新功能是必需的,其中每个用户的社交媒体平台(SMP)链接都需要与用户对象一起进行跟踪。

这里的关键还在于,必须在对现有代码的干预最少的情况下实现此功能。

对于WeakMaps,可以通过以下方式实现。

我为Twitter,Facebook,LinkedIn添加了三个单独的WeakMap。

/*
WeakMaps for Social Media Platforms (SMPs).
Could be replaced by a single Map which can grow
dynamically based on different SMP names . . . anyway...
*/
var sm_platform_twitter = new WeakMap();
var sm_platform_facebook = new WeakMap();
var sm_platform_linkedin = new WeakMap();

添加了一个辅助函数,getSMPWeakMap只是为了返回与给定SMP名称关联的WeakMap。

/**
Returns the WeakMap for the given SMP.
*/
var getSMPWeakMap = (sm_platform) => {
    if(sm_platform == "Twitter") {
        return sm_platform_twitter;
    }
    else if(sm_platform == "Facebook") {
        return sm_platform_facebook;
    }
    else if(sm_platform == "LinkedIn") {
        return sm_platform_linkedin;
    }
    return undefined;
}

将用户SMP链接添加到给定SMP WeakMap的功能。

/**
Adds a SMP link associated with a given User. The User must be already added to the Collection.
*/
var addUserSocialMediaLink = (username, sm_platform, sm_link) => {
    let user = getUser(username);
    let sm_platform_weakmap = getSMPWeakMap(sm_platform);
    if(user && sm_platform_weakmap) {
        sm_platform_weakmap.set(user, sm_link);
    }
}

仅打印给定SMP上存在的用户的功能。

/**
Prints the User's fullname and corresponding SMP link of only those Users which are on the given SMP.
*/
var printSMPUsers = (sm_platform) => {
    let sm_platform_weakmap = getSMPWeakMap(sm_platform);
    console.log(`Users of ${sm_platform}:`)
    users.forEach((user)=>{
        if(sm_platform_weakmap.has(user)) {
            console.log(`\t${user.fullname} : ${sm_platform_weakmap.get(user)}`)
        }
    });
}

现在,您可以为用户添加SMP链接,也可以使每个用户在多个SMP上都有一个链接。

...继续前面的示例,我向用户添加了SMP链接,为用户Bill和Sarah添加了多个链接,然后分别为每个SMP打印链接: 向用户添加SMP链接并显示它们

现在说users通过调用将用户从地图中删除deleteUser。这将删除对用户对象的唯一引用。反过来,这也将从所有/所有SMP WeakMap(通过垃圾回收)中清除SMP链接,因为如果没有User Object,则无法访问其任何SMP链接。

...继续上例,我删除了用户Bill,然后打印出与他关联的SMP的链接:

从地图上删除用户Bill也会删除SMP链接

无需任何其他代码即可单独删除SMP链接,而无需修改该功能之前的现有代码。

如果有其他方法可以添加带有/不带有WeakMaps的此功能,请随时发表评论。


_____尼斯______
亚历山大
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.