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

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

在我看来:

weakmap.set(key, value);

...只是拐弯抹角地说

key.value = value;

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


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

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

从根本上

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

这是可行的,但它有一个内存泄漏——我们现在跟踪传递给函数的每个库对象,以防止库对象被垃圾收集。相反,我们可以使用一个弱映射:

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启用,包括:

Keeping private data about a specific object and only giving access to it to people with a reference to the Map. A more ad-hoc approach is coming with the private-symbols proposal but that's a long time from now. Keeping data about library objects without changing them or incurring overhead. Keeping data about a small set of objects where many objects of the type exist to not incur problems with hidden classes JS engines use for objects of the same type. Keeping data about host objects like DOM nodes in the browser. Adding a capability to an object from the outside (like the event emitter example in the other answer).

让我们看看真正的用途

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

假设你是Node.js,你有Promise对象——现在你想要跟踪所有当前被拒绝的Promise——但是,你不想让它们在没有引用的情况下被垃圾收集。

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

进入弱地图

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

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

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

我使用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非常适合memo,因为一旦对象(用作键)被垃圾收集,WeakMap上的计算值也会被垃圾收集。


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

管理用户集合

我从一个User对象开始,它的属性包括全名、用户名、年龄、性别和一个名为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,以保存多个用户的集合,这些用户由用户名输入。

/**
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,在整个进程中只有用户映射具有对用户对象的引用。没有其他对单个用户对象的引用。

在交互式NodeJS shell中运行这段代码,就像一个例子,我添加了四个用户并打印他们:

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

现在,假设需要一个新功能,其中每个用户的社交媒体平台(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上拥有一个链接。

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

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

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

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

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


我认为这是非常有用的检查连接收入在应用程序套接字。 另一种情况,'Weak Collection'是有用的: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