ES6 WeakMap的实际用途是什么?

val*_*man 374 javascript weakmap ecmascript-6

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

由于弱映射的键会创建对其相应值的强引用,因此只要其键仍处于活动状态,确保插入到弱映射中的值永远不会消失,它不能用于备注表,缓存或您通常使用弱引用的任何其他内容,具有弱值的映射等.

在我看来,这个:

weakmap.set(key, value);
Run Code Online (Sandbox Code Playgroud)

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

key.value = value;
Run Code Online (Sandbox Code Playgroud)

我错过了哪些具体的用例?

Ben*_*aum 477

从根本上

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

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

假设我正在使用一个给我一个特定对象的API:

var obj = getObjectFromLibrary();
Run Code Online (Sandbox Code Playgroud)

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

function useObj(obj){
   doSomethingWith(obj);
}
Run Code Online (Sandbox Code Playgroud)

我想跟踪使用某个对象调用该方法的次数,并报告它是否发生超过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);
}
Run Code Online (Sandbox Code Playgroud)

这有效,但它有内存泄漏 - 我们现在跟踪传递给函数的每个库对象,这使得库对象不会被垃圾收集.相反 - 我们可以使用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);
}
Run Code Online (Sandbox Code Playgroud)

内存泄漏消失了.

用例

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

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

让我们来看看真实的用途

它可以用于从外部扩展对象.让我们从Node.js的真实世界中给出一个实用的(适应的,真实的 - 来说明一点).

假设你是Node.js并且你有Promise对象 - 现在你想要跟踪所有当前被拒绝的承诺 - 但是,如果没有引用它们,你希望它们被垃圾收集.

现在,您希望出于显而易见的原因向本机对象添加属性 - 因此您将陷入困境.如果您继续引用承诺,则会导致内存泄漏,因为不会发生垃圾回收.如果您不保留参考文献,则无法保存有关个人承诺的其他信息.任何涉及保存承诺ID的方案本身就意味着您需要对它的引用.

输入WeakMaps

WeakMaps意味着密钥很弱.没有办法枚举弱地图或获取其所有值.在弱映射中,您可以基于密钥存储数据,以及何时密钥被垃圾收集,因此值.

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

这是用来实现未处理拒绝钩由安东诺夫佩特卡作为:

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
});
Run Code Online (Sandbox Code Playgroud)

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

  • @ ltamajs4当然,在使用`Map`而不是`WeakMap`的`useObj`示例中,我们使用传入的对象作为映射键.该对象永远不会从地图中删除(因为我们不知道何时这样做)因此始终存在对它的引用,并且它永远不会被垃圾收集.在WeakMap示例中,只要对象的所有其他引用都消失了 - 就可以从`WeakMap`中清除对象.如果您仍然不确定我的意思,请告诉我 (14认同)
  • 你好!你能否告诉我示例代码的哪一部分导致内存泄漏? (8认同)
  • @Benjamin,我们需要区分对内存敏感的缓存的需求和对 data_object 元组的需求。**不要将这两个单独的要求混为一谈。** 您的“叫”示例最好使用 https://jsfiddle.net/f2efbm7z/ 编写,并且它不会演示弱映射的使用。事实上,总共有**6**种方式可以更好地编写它,我将在下面列出。 (3认同)

axe*_*uch 47

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

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

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


在阅读下一步之前

我现在意识到我的强调并不是解决这个问题的最佳方法,正如本杰明·格伦鲍姆所指出的那样(看看他的答案,如果它还没有超过我的答案: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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我不太明白你会怎么用这个.当不再引用时,它会导致observable与绑定到它的事件一起崩溃.我倾向于遇到的问题是不再引用Observer.我认为这里的解决方案只解决了一半的问题.我不认为你可以解决WeakMap的观察者问题,因为它不可迭代. (2认同)

Mic*_*ski 16

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
Run Code Online (Sandbox Code Playgroud)

我们使用该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;
    }());
})()
Run Code Online (Sandbox Code Playgroud)

  • Re"weakmap适用于封装和信息隐藏".只因为你可以,并不意味着你应该.即使在发明弱映射之前,Javascript也有默认的封装和信息隐藏方式.截至目前,[有6种方法可以做到](/sf/ask/2058925571/#comment79534889_29416340).使用weakmap进行封装是一个丑陋的facepalm. (3认同)

Ric*_*ler 10

WeakMap用于缓存无忧的memoization函数,这些函数将不可变对象作为参数.

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

这是一个例子:

// 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);
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>
Run Code Online (Sandbox Code Playgroud)

有几点需要注意:

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


Yus*_*han 10

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

\n
let john = { name: "yusuf" };\n\nlet map = new Map();\nmap.set(yusuf, "xyz"); //here "yusuf" is the key and "xyz" is value\n\nyusuf= null; // overwrite the reference\n\n// the object previously referenced by yusuf is stored inside the array\n// therefore it won\'t be garbage-collected\n// we can get it using map.keys()\n
Run Code Online (Sandbox Code Playgroud)\n

但弱映射的情况并非如此,这里内存将被释放

\n
let john = { name: "yusuf" };\n\nlet map = new WeakMap();\nmap.set(yusuf, "...");\n\nyusuf= null; // overwrite the reference\n\n// yusuf is removed from memory!\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

如果我们\xe2\x80\x99正在使用一个\xe2\x80\x9c属于\xe2\x80\x9d到另一个代码(甚至可能是第三方库)的对象,并且想要存储一些与其关联的数据,那么应该仅当对象处于活动状态时才存在 \xe2\x80\x93 那么 WeakMap 正是 \xe2\x80\x99s 所需要的。

\n

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

\n
weakMap.set(yusuf, "secret documents");\n// if yusuf dies, secret documents will be destroyed automatically\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n


Jac*_*fin 9

弱映射可用于存储有关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[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"
  )   // }
);
Run Code Online (Sandbox Code Playgroud)

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"
  )   // }
);
Run Code Online (Sandbox Code Playgroud)

除了弱映射版本更长的事实之外,这种差异看起来可以忽略不计,但是上面显示的两段代码之间存在很大的差异。在没有弱映射的第一段代码中,这段代码存储DOM元素之间的所有引用。这样可以防止DOM元素被垃圾回收。Math.pow(i, 2) % len]似乎没有人会使用,但是再三考虑:很多生产代码中都有DOM引用,这些引用在整个文档中都会出现。现在,对于第二段代码,由于对元素的所有引用都很弱,因此在删除节点时,浏览器能够确定未使用该节点(您的代码无法访问该节点),并且因此将其从内存中删除。之所以要关注内存使用情况和内存锚点(例如未使用的元素保留在内存中的第一个代码段)之类的原因,是因为更多的内存使用率意味着更多的浏览器尝试使用GC(尝试将内存释放给避免浏览器崩溃)意味着浏览体验变慢,有时还会导致浏览器崩溃。

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

〜编码愉快!

  • @Pacerier感谢您的热心反馈,但是将`elements`设置为null将**不允许**浏览器在第一个代码段中使用GC元素。这是因为您在元素上设置了自定义属性,然后仍可以获取那些元素,并且仍可以访问其自定义属性,从而防止对其中的任何元素进行GC处理。可以将其视为一串金属环。只要您可以访问链中的至少一个链接,就可以抓住链中的该链接,从而防止整个物品链掉入深渊。 (2认同)
  • @LukaszMatysiak 这是一个更短、更跨浏览器的版本:`""+true`。我不会对代码进行此更改,因为代码的目标是人类可读,而不是最大程度地节省空间。并不是每个人都像你我一样了解 JS。有些初学者只是想开始学习这门语言。当我们炫耀自己拥有先进的 JS 知识时,这对他们没有任何帮助。 (2认同)

ele*_*rat 8

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

管理用户集合

我从一个UserObject开始,它的属性包括 a fullnameusername、和一个被调用的方法agegender该方法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}`);
}
Run Code Online (Sandbox Code Playgroud)

然后,我添加了一个 Map 调用users以保留多个用户的集合,这些用户由username.

/**
Collection of Users, keyed by username.
*/
var users = new Map();
Run Code Online (Sandbox Code Playgroud)

添加集合还需要辅助函数来添加、获取、删除用户,甚至为了完整起见打印所有用户的函数。

/**
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();
    });
}
Run Code Online (Sandbox Code Playgroud)

上面所有的代码都运行在NodeJS 中users在整个过程中只有Map 具有对用户对象的引用。没有对单个用户对象的其他引用。

在交互式 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();
Run Code Online (Sandbox Code Playgroud)

添加一个辅助函数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;
}
Run Code Online (Sandbox Code Playgroud)

将用户 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

仅打印给定 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)}`)
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

您现在可以为用户添加 SMP 链接,还可以让每个用户在多个 SMP 上拥有一个链接。

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

现在假设users通过调用从Map 中删除了一个用户deleteUser。这将删除对用户对象的唯一引用。这反过来也会从任何/所有 SMP WeakMaps(通过垃圾收集)清​​除 SMP 链接,因为没有用户对象,就无法访​​问其任何 SMP 链接。

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

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

不需要任何额外的代码来单独删除 SMP 链接,并且在此功能之前的现有代码没有进行任何修改。

如果有任何其他方式可以在有/没有 WeakMaps 的情况下添加此功能,请随时发表评论。

  • _____好的______ (2认同)
  • 谢谢,这是我读过的第一个清晰的例子,解释了这些何时会派上用场。 (2认同)