ES6 WeakMap 的实际用途是啥?

Posted

技术标签:

【中文标题】ES6 WeakMap 的实际用途是啥?【英文标题】:What are the actual uses of ES6 WeakMap?ES6 WeakMap 的实际用途是什么? 【发布时间】:2015-06-07 10:10:40 【问题描述】:

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

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

在我看来是这样的:

weakmap.set(key, value);

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

key.value = value;

我缺少哪些具体用例?

【问题讨论】:

A blog post. 还有一个——ilikekillnerds.com/2015/02/what-are-weakmaps-in-es6 真实世界用例:为 DOM 节点存储自定义数据。 您提到的所有弱引用用例也非常重要。由于它们引入了不确定性,因此很难将它们添加到语言中。 Mark Miller 和其他人在弱引用方面做了很多工作,我认为他们最终会出现。最终 WeakMaps 可用于检测内存泄漏:stevehanov.ca/blog/?id=148 【参考方案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中,以对象为key,当对象被垃圾回收时,数据也会自动消失。

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

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

【讨论】:

【参考方案2】:

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

【讨论】:

【参考方案3】:

????????

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

??? ??????????

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

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

~编码愉快!

【讨论】:

感谢您的明确解释。一个例子胜过任何言语。 @lolzery, Re "这可以防止 DOM 元素被垃圾回收",您只需elements 设置为 null 和你完成了:它将被 GCed。 & Re "DOM 引用在整个文档中反弹",一点都不重要:一旦主链接elements 消失,所有循环引用都将是GCed。如果您的元素保留了对它不需要的元素的引用,请修复代码并在使用完后将引用设置为 null。它会被GCed。 不需要弱图 @Pacerier 感谢您的热情反馈,但是将 elements 设置为 null 将不允许允许浏览器在第一次 sn-p 情况下对元素进行 GC。这是因为您在元素上设置了自定义属性,然后仍然可以获取这些元素,并且仍然可以访问它们的自定义属性,从而防止它们中的任何一个被 GC'ed。把它想象成一串金属环。只要您可以访问链条中的至少一个环节,您就可以抓住链条中的那个环节,从而防止整个链条的物品掉入深渊。 带有名为 vars 的 dunder 的生产代码让我呕吐 @LukaszMatysiak 这是一个更短且跨浏览器的版本:""+true。我不会对代码进行此更改,因为代码的目标是人类可读,而不是最大限度地节省空间。不是每个人都像你和我一样了解 JS。有些初学者只是想开始学习这门语言。当我们炫耀我们对 JS 的高级知识时,这对他们一点帮助也没有。【参考方案4】:

我有这个基于简单功能的 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 的 Map 来保存由 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 中运行此代码,就像我添加四个用户并打印它们一样作为示例:

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

现在说需要一项新功能,其中需要跟踪每个用户的社交媒体平台 (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 的链接:

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

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

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

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

【讨论】:

_____nice______ 谢谢,这是我读到的第一个清晰的例子,解释了这些什么时候会派上用场。【参考方案5】:

从根本上说

WeakMaps 提供了一种从外部扩展对象而不干扰垃圾回收的方法。 每当您想要扩展对象但因为它是密封的(或来自外部源)而无法扩展时,WeakMap可以申请。

WeakMap 是一个映射(字典),其中 keys 是弱的 - 也就是说,如果对 key 的所有引用都丢失并且没有更多对value - value 可以被垃圾回收。让我们先通过例子来展示,然后稍微解释一下,最后以实际使用结束。

假设我正在使用一个给我特定对象的 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 的 ID 的方案都意味着您需要对它的引用。

输入弱映射

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

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

这被 Petka Antonov 用来实现unhandled rejection hooks 为this:

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

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

【讨论】:

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

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() 方法来检索与对象关联的项目。 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 适用于封装和信息隐藏”。仅仅因为你可以,并不意味着你应该。甚至在 Weakmap 被发明之前,Javascript 就有默认的封装和信息隐藏方式。截至目前,有literally 6 ways to do it。用weakmap做封装是丑脸。【参考方案7】:

我使用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);
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"&gt;&lt;/script&gt;

需要注意的几点:

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

【讨论】:

这是对weakmap的有效使用,只要memoization缓存是内存敏感的,而不是在整个obj/函数生命周期中持续存在。如果“记忆缓存”意味着要在整个 obj/函数生命周期内保持不变,那么weakmap 是错误的选择:改用any of the 6 default javascript encapsulation techniques。【参考方案8】:

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

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

【讨论】:

我不太明白你会如何使用它。当不再引用时,它会导致 observable 连同绑定到它的事件一起崩溃。我倾向于遇到的问题是不再引用观察者。我认为这里的解决方案只解决了一半的问题。我认为您无法使用 Wea​​kMap 解决观察者问题,因为它不可迭代。 双缓冲事件侦听器在其他语言中可能很快,但在这种情况下它只是简单的深奥和缓慢。那是我的三分钱。 @axelduch,哇,这个监听器-句柄神话一直被兜售到 Javascript 社区,获得了 40 个赞!要了解为什么这个答案完全错误,请参阅***.com/a/156618/632951 下的 cmets @Pacerier 更新了答案,感谢您的反馈 @axelduch,是的,那里也有一个参考。

以上是关于ES6 WeakMap 的实际用途是啥?的主要内容,如果未能解决你的问题,请参考以下文章

ES6 import 语句中花括号的用途是啥?

ES6 WeakMap和WeakSet的使用场景

ES6:数据结构Set和Map&WeakSet和WeakMap

ES6 Map WeakMap Set WeakSet

ES6:数据结构Set和Map&WeakSet和WeakMap

ES6通过WeakMap解决内存泄漏问题