ES6 WeakMap 的实际用途是什么?

What are the actual uses of ES6 WeakMap?

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

由于weak map的key创建了对其对应值的强引用,确保插入到weak map中的值只要其永远不会消失key 仍然存在,它不能用于备忘录表、缓存或您通常使用弱引用、具有弱值的映射等的任何其他内容。

在我看来是这样的:

weakmap.set(key, value);

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

key.value = value;

我遗漏了哪些具体用例?

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

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

但是从更抽象的角度来看,WeakMap 尤其强大,可以使对任何东西的访问非物质化,您不需要命名空间来隔离其成员,因为它的本质已经暗示了这一点结构体。我很确定你可以通过替换笨拙的冗余对象键来做一些主要的内存改进(即使解构为你工作)。


阅读下一篇文章之前

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


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

Full source here, it's about listeners management I talked about above (you can also take a look at the 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);
    }
}

基本

WeakMaps 提供了一种在不干扰垃圾回收的情况下从外部扩展对象的方法。 当你想扩展一个对象但因为它是密封的而不能扩展时 - 或者从外部源 - 可以应用 Wea​​kMap。

WeakMap 是一个映射(字典),其中 keys 是弱的 - 也就是说,如果对 key 的所有引用都丢失了并且不再有对该值的引用 - 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);
}

并且内存泄漏消失了。

用例

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

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

来看一个实际使用

它可用于从外部扩展对象。让我们从 Node.js.

的现实世界中举一个实际的(改编的,有点真实的 - 说明一点)例子

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

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

输入 WeakMaps

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

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

这用于实现unhandled rejection hooks by Petka Antonov as 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
});

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

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

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 尝试(试图释放内存以avert a browser crash) 意味着浏览体验变慢,有时还会导致浏览器崩溃。

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

~ 编码愉快!

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

记忆是一种奇特的说法 "after you compute the value, cache it so you don't have to compute it again"。

这是一个例子:

// 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 objects return new objects (with a new pointer) 当您修改它们时,将它们用作 WeakMap 中的键可以保证相同的计算值。
  • WeakMap 非常适合备忘录,因为一旦对象(用作键)被垃圾回收,WeakMap 上的计算值也会被回收。

我有这个基于简单功能的 WeakMaps 使用 case/Example。

管理用户集合

我从一个 User 对象开始,它的属性包括 fullnameusernameagegender 和一个名为 [=22] 的方法=] 打印其他属性的人类可读摘要。

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

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

/**
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) link 需要与用户对象一起跟踪。

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

这可以通过以下方式使用 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 简单地添加到 return 与给定 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 link 添加到给定 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 link,每个用户还可以在多个 SMP 上拥有 link。

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

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

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

不需要任何附加代码来单独删除 SMP link 并且此功能之前的现有代码无论如何都没有修改。

如果有任何其他方法可以添加此功能with/withoutWeakMaps欢迎随时评论。

我觉得在应用程序套接字中查看连接收入很有帮助。 另一种情况,'Weak Collection' 很有用:https://javascript.info/task/recipients-read

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

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