ES6 WeakMap的实际用途是什么?

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

由于弱映射的键创建了对其对应值的强引用,确保插入到弱映射中的值只要其键仍然存在,从来没有就会消失,因此它不能用于备忘录表、缓存或其他通常使用弱引用、弱值映射等的任何东西。

在我看来:

weakmap.set(key, value);

...只是拐弯抹角地说

key.value = value;

我遗漏了哪些具体的用例?

96027 次浏览

这个答案似乎是有偏见的,在现实世界中是不可用的。请原原本本地阅读,不要把它看作是一种实际的选择,而只是一种实验

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

但是从更抽象的角度来看,WeakMap对于基本上任何东西的非实体化访问都是特别强大的,你不需要一个名称空间来隔离它的成员,因为它已经被这个结构的性质所暗示。我很确定你可以通过替换尴尬的冗余对象键来做一些重大的内存改进(即使解构可以为你工作)。


在阅读接下来的内容之前

我现在意识到我的强调并不是解决问题的最佳方法,正如本杰明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);
}
}

从根本上

weakmap提供了一种从外部扩展对象而不干扰垃圾收集的方法。当你想要扩展一个对象,但因为它是密封的——或者从外部源——而不能扩展时,可以应用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);
}

内存泄漏也消失了。

用例

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

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

让我们看看真正的用途

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

假设你是Node.js,你有Promise对象——现在你想要跟踪所有当前被拒绝的承诺——然而,你确实想要防止它们在没有引用的情况下被垃圾收集。

现在,你想要为本机对象添加属性,原因很明显——所以你被卡住了。如果保持对承诺的引用,就会导致内存泄漏,因为不会发生垃圾收集。如果你不保留推荐信,那么你就无法保存关于个人承诺的额外信息。任何涉及保存承诺ID的方案本质上都意味着需要对它的引用。

进入WeakMaps

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

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

这被Petka 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
});

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

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 map可用于存储关于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"
)   // }
);

𝗧𝗵𝗲𝗗𝗶𝗳𝗳𝗲𝗿𝗲𝗻𝗰𝗲

除了弱映射版本更长之外,这种差异看起来可以忽略不计,但是上面所示的两段代码之间有一个主要的差异。在第一个代码片段中,没有弱映射,这段代码以各种方式存储DOM元素之间的引用。这可以防止DOM元素被垃圾收集。(i * i) % len可能看起来像一个没有人会使用的奇怪的东西,但再想想:大量的产品代码都有DOM引用,这些引用在文档中到处都是。现在,对于第二段代码,因为所有对元素的引用都是弱的,当您删除一个节点时,浏览器能够确定该节点没有被使用(您的代码无法到达),从而从内存中删除它。为什么您应该关注内存使用和内存锚(像第一个代码片段中未使用的元素保存在内存中)的原因是因为更多的内存使用意味着更多的浏览器gc尝试(试图释放内存以避免浏览器崩溃)意味着更慢的浏览体验,有时还会导致浏览器崩溃。

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

~快乐编码!

我使用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>

A few things to note:

  • Immutable.js objects return new objects (with a new pointer) when you modify them so using them as keys in a WeakMap is guarantees the same computed value.
  • The WeakMap is great for memos because once the object (used as the key) gets garbage collected, so does the computed value on the WeakMap.

我有一个简单的基于弱地图功能的用例/示例。

管理用户集合

我从一个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();

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

/**
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中运行这段代码,就像一个例子,我添加了四个用户并打印他们: 添加和打印用户 < / p >

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

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

这里的关键还在于,该特性的实现必须对现有代码进行最少的干预。

这可以通过以下方式使用weakmap实现。

我为Twitter, Facebook, LinkedIn添加了三个单独的弱地图。

/*
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名称相关联的弱映射。

/**
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弱映射的函数。

/**
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上拥有一个链接。

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

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

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

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

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

如果有任何其他方式添加这个功能,或没有弱地图,请随时评论。

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

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