ES6 WeakMap的实际用途是什么?

时间:2015-04-02 12:39:06

标签: javascript ecmascript-6 weakmap

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

由于弱映射的键会创建对其相应值的强引用,因此只要其键仍处于活动状态,确保已插入到弱映射中的值永远不会消失,它不能用于备忘录表,缓存或其他任何你通常使用弱引用,弱值映射等的东西。

在我看来,这是:

weakmap.set(key, value);

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

key.value = value;

我错过了哪些具体用例?

7 个答案:

答案 0 :(得分:441)

从根本上说

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

WeakMap是一个地图(字典),其中很弱 - 也就是说,如果对的所有引用都丢失了,并且没有更多引用值 - 可以被垃圾收集。让我们先通过例子说明这一点,然后再解释一下,最后以实际用途结束。

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

var obj = getObjectFromLibrary();

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

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

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

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个对象 - 现在你想跟踪所有当前被拒绝的承诺 - 但是,想要保留它们在没有引用的情况下收集垃圾。

现在,想要将属性添加到本机对象中,原因显而易见 - 所以你被卡住了。如果您继续引用承诺,则会导致内存泄漏,因为不会发生垃圾回收。如果您不保留参考文献,则无法保存有关个人承诺的其他信息。任何涉及保存承诺ID的方案本身都意味着您需要引用它。

输入WeakMaps

WeakMaps意味着很弱。没有办法枚举弱地图或获取其所有值。在弱映射中,您可以基于密钥存储数据,并且当密钥被垃圾收集时,也可以使用值。

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

Petka Antonov使用unhandled rejection hooks作为this来实现{{3}}:

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

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

答案 1 :(得分:46)

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

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

但从更抽象的角度来看,WeakMap特别强大,可以取消对基本上任何东西的访问,你不需要一个命名空间来隔离它的成员,因为它已经暗示了它的本质结构体。我非常确定你可以通过替换笨拙的冗余对象密钥来做一些重大的内存改进(尽管解构会为你工作)。


在阅读下一步之前

我现在意识到我的强调并不是解决问题的最佳方法,正如Benjamin Gruenbaum所指出的那样(查看他的答案,如果它还没有超出我的意思:p),这个问题无法用常规Map解决,因为它会泄露,因此WeakMap的主要优势在于它不会干扰垃圾收集,因为它们没有保留参考。


以下是我的同事的实际代码(感谢him分享)

Full source here,关于我上面谈过的听众管理(你也可以看看specs

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 :(得分:14)

WeakMap适用于封装和信息隐藏

WeakMap仅适用于ES6及以上版本。 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()方法检索与对象关联的项。 WeakMap s的有趣之处在于它对地图内部的键具有弱引用。弱引用意味着如果对象被销毁,垃圾收集器将从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;
    }());
})()

答案 3 :(得分:7)

我使用WeakMap来获取以不可变对象作为参数的函数的无忧记忆缓存。

记忆是一种奇特的说法"在你计算出价值后,将其缓存,这样你就不必再次计算它了。

以下是一个例子:



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

<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>
&#13;
&#13;
&#13;

有几点需要注意:

  • 当您修改它们时,Immutable.js对象返回新对象(使用新指针),因此将它们用作WeakMap中的键可保证相同的计算值。
  • WeakMap非常适合备忘录,因为一旦对象(用作密钥)被垃圾收集,WeakMap上的计算值也会被收集。

答案 4 :(得分:6)

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

没有WeakMaps或WeakSets:

&#13;
&#13;
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"
  )   // }
);
&#13;
&#13;
&#13;

使用WeakMaps和WeakSets:

&#13;
&#13;
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"
  )   // }
);
&#13;
&#13;
&#13;

差异

除了弱图版本更长的事实之外,差异可能看起来可以忽略不计,但是上面显示的两段代码之间存在重大差异。在第一段代码中,没有弱映射,这段代码存储了DOM元素之间每个方向的引用。这可以防止DOM元素被垃圾收集。 Math.pow(i, 2) % len]可能看起来像一个没有人会使用的奇怪的东西,但是再想一想:大量的生产代码都有DOM引用,它们会在整个文档中反弹。现在,对于第二段代码,因为对元素的所有引用都很弱,所以当您删除节点时,浏览器能够确定该节点未被使用(代码无法访问),并且从内存中删除它。为什么你应该关注内存使用和内存锚点(比如第一段代码片段,其中未使用的元素保存在内存中)是因为更多的内存使用意味着更多的浏览器GC尝试(试图释放内存到避免浏览器崩溃)意味着浏览体验变慢,有时浏览器崩溃。

对于这些的填充,我会推荐我自己的库(found here @ github)。它是一个非常轻量级的库,它可以简单地填充它,而不需要在其他polyfill中找到任何过于复杂的框架。

〜快乐的编码!

答案 5 :(得分: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}`);
}

然后我添加了一个名为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 )的过程中,只有users映射在整个过程中具有对用户对象的引用。没有其他参考单独的用户对象。

以一个交互式NodeJS shell运行此代码,就像一个示例一样,我添加了四个用户并打印它们: Adding and printing users

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

现在说需要一个新功能,其中每个用户的社交媒体平台(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的链接: Adding SMP links to the users and displaying them

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

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

Deleting user Bill from the Map removes the SMP links as well

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

如果有其他添加或不添加WeakMaps的功能,请随时发表评论。

答案 6 :(得分:1)

我认为这对于检查应用程序套接字中的连接收入非常有帮助。 另一种情况是“弱集合”很有用:https://javascript.info/task/recipients-read