471

What are the actual uses of the WeakMap data structure introduced in ECMAScript 6?

Since a key of a weak map creates a strong reference to its corresponding value, ensuring that a value which has been inserted into a weak map will never disappear as long as its key is still alive, it can't be used for memo tables, caches or anything else that you would normally use weak references, maps with weak values, etc. for.

It seems to me that this:

weakmap.set(key, value);

...is just a roundabout way of saying this:

key.value = value;

What concrete use cases am I missing?

4

8 回答 8

601

从根本上说

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

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

并且内存泄漏消失了。

用例

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

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

来看一个真实的使用

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

假设你是 Node.js 并且你有Promise对象——现在你想要跟踪所有当前被拒绝的 Promise——但是,你不想阻止它们被垃圾收集,以防不存在对它们的引用。

现在,出于显而易见的原因,您不想向本机对象添加属性 - 所以您被卡住了。如果您保留对 Promise 的引用,则会导致内存泄漏,因为不会发生垃圾收集。如果您不保留引用,则无法保存有关单个承诺的其他信息。任何涉及保存 Promise 的 ID 的方案都意味着您需要对其进行引用。

输入弱映射

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

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

这被P​​etka 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 的信息保存在 map 中,并且可以知道何时处理了被拒绝的 Promise。

于 2015-04-02T15:09:00.997 回答
49

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

一个用例可能是将它用作听众的字典,我有一个同事这样做。这非常有帮助,因为任何听众都直接针对这种做事方式。再见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);
    }
}
于 2015-04-02T12:48:53.230 回答
24

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()方法来检索与对象关联的项目。s的有趣之处WeakMap在于它持有对映射内键的弱引用。弱引用意味着如果对象被销毁,垃圾收集器将从中删除整个条目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;
    }());
})()
于 2016-10-25T13:45:55.333 回答
16

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

除了weakmap版本更长之外,差异看起来可以忽略不计,但是上面显示的两段代码之间存在重大差异。在没有弱映射的第一段代码中,这段代码存储了 DOM 元素之间的任何方式的引用。这可以防止 DOM 元素被垃圾收集。(i * i) % len可能看起来像一个没人会使用的奇怪球,但再想一想:大量生产代码的 DOM 引用会在整个文档中反弹。现在,对于第二段代码,由于对元素的所有引用都很弱,当您删除节点时,浏览器能够确定该节点未使用(您的代码无法访问),并且因此将其从内存中删除。您应该关注内存使用和内存锚点(例如将未使用元素保存在内存中的第一段代码)的原因是因为更多的内存使用意味着更多的浏览器 GC 尝试(尝试释放内存以避免浏览器崩溃)意味着较慢的浏览体验,有时还会导致浏览器崩溃。

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

〜快乐编码!

于 2017-08-06T22:54:50.747 回答
14

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>

需要注意的几点:

  • Immutable.js 对象在您修改它们时返回新对象(带有新指针),因此将它们用作 WeakMap 中的键可以保证相同的计算值。
  • WeakMap 非常适合备忘录,因为一旦对象(用作键)被垃圾收集,WeakMap 上的计算值也会被收集。
于 2017-09-17T11:25:08.440 回答
10

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

管理用户集合

我从一个UserObject 开始,它的属性包括fullname, username,和一个调用的方法age,该方法打印出其他属性的人类可读摘要。genderprint

/**
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 的 Mapusers来保存多个用户的集合,这些用户由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 在整个过程中引用了用户对象。没有其他对单个用户对象的引用。

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

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

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

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

这可以通过以下方式使用 Wea​​kMaps。

我为 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 WeakMaps(通过垃圾收集)中清除 SMP 链接,因为没有用户对象,就无法访​​问其任何 SMP 链接。

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

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

不需要任何额外的代码单独删除 SMP 链接,并且此功能之前的现有代码无论如何都没有修改。

如果有任何其他方法可以使用/不使用 Wea​​kMaps 添加此功能,请随时发表评论。

于 2020-03-04T10:18:07.967 回答
1

WEAKMAP:请记住,weakMap 是关于内存分配和垃圾收集的,仅当您将值存储在键值对数组、映射、集合等中时,才与 javascript 中对象类型的键相关......分配给所有键的内存值对,即使您删除或将 null 设置为该键,此内存也不会空闲 将其视为强映射键强烈附加到内存下面是示例

let john = { name: "yusuf" };

let map = new Map();
map.set(yusuf, "xyz"); //here "yusuf" is the key and "xyz" is value

yusuf= null; // overwrite the reference

// the object previously referenced by yusuf is stored inside the array
// therefore it won't be garbage-collected
// we can get it using map.keys()

但是这里的weakMap并非如此,内存将是免费的

let john = { name: "yusuf" };

let map = new WeakMap();
map.set(yusuf, "...");

yusuf= null; // overwrite the reference

// yusuf is removed from memory!

用例:您将在您希望以更有效的方式管理内存的 javascript 中使用

如果我们正在使用一个“属于”另一个代码的对象,甚至可能是第三方库,并且想要存储一些与之关联的数据,那么这些数据应该只在对象处于活动状态时才存在——那么 WeakMap 正是需要。

我们将数据放到 WeakMap 中,使用对象作为键,当对象被垃圾回收时,这些数据也会自动消失。

weakMap.set(yusuf, "secret documents");
// if yusuf dies, secret documents will be destroyed automatically

我参考了这篇很棒的文章:https ://javascript.info/weakmap-weakset

于 2021-11-04T10:43:34.597 回答
0

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

于 2020-11-11T18:09:23.007 回答