【每日一题NO.81】WeakMap和WeakSet

前端印记

共 5470字,需浏览 11分钟

 ·

2021-11-09 00:55

写在前边

关于阮一峰老师的《ES6 - WeakSet》等系列文章,我做了带读视频

你可以点击阅读原文,精准空降看详情哦~


(PS:后续也会不定期更新)



!=======正经分割线=======!



目录:

  • 为什么WeakMap/WeakSet的键只能是对象

  • 应用场景

    • 通过 WeakMap 缓存计算结果

    • 额外的数据

  • WeakMap WeakSet

    • WeakMap

    • WeakSet

  • 总结

为什么WeakMap/WeakSet的键只能是对象

WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

为什么会有这种限制?

1、是为了保证只有通过键对象的引用来取得值。

const m = new WeakMap();
m.set({}, 100);
// 由于 {} 没有在其他地方引用,所以在垃圾回收时,这个值也会被回收。

const a = {};
m.set(a, 100);
// 如果使用这种方式,则不会被回收。因为 {} 有 a 变量在引用它。

a = null;
// 将 a 置为空后,m 里的值 100 在垃圾回收时将会被回收。

如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的字符串字面量是一个相等的字符串了。

const a = {};
// 在创建对象时,分配了一块内存,并把这块内存的地址传给 a
m.set(a, 100);
// 执行 set 操作时,实际上是将 a 指向的内存地址和 100 关联起来

const a = "abc";
// 由于基本数据类型在传递时,传递的是值,而不是引用。
m.set(a, 100);
// 所以执行 set 操作时,实际上是将新的 'abc' 和 100 关联起来,而不是原来 a 变量指向的那个。
// 那这样就会有问题,m 里存储的永远是没有被引用的键,随时都会被回收。

应用场景

通过 WeakMap 缓存计算结果

另外一个普通的例子是缓存:当一个函数的结果需要被记住(所谓的缓存),这样在后续的对同一个对象的调用时,就是可以重用这个被缓存的结果。

可以使用 Map 来存储结果,就像这样:

// cache.js
let cache = new Map();

// 计算结果并记住
function process(obj{
  if (!cache.has(obj)) {
    let result = /* calculations of the result for */ obj;
    cache.set(obj, reesult);
  }
  return cache.get(obj);
}

// main.js
let obj = {
  /* 假设有个对象 */
};
let result1 = process(obj); //计算完成

// 当不再需要这个对象时
obj = null;

alert(cache.size); //1 该对象依然在cache 中,并占据着内存

对于多次调用同一个对象,它只需在第一次调用时计算出结果,之后的调用直接从 cache 中获取。这样做的缺点是,当我们不在需要这个对象的时候需要清理 cache。如果我们用 WeakMap 代替 Map,这个问题便会消失:当对象被垃圾回收时,对应的缓存结果也会被自动地从内存中清除。

// cache.js
let cache = new WeakMap();

function process(obj{
  if (!cache.has(obj)) {
    let result = /* calculations of the result for */ obj;
    cache.set(obj, reesult);
  }
  return cache.get(obj);
}
// ? main.js
let obj = {
  /* some object */
};

let result1 = process(obj);
let result2 = process(obj);

// ……稍后,我们不再需要这个对象时:
obj = null;
// 当 obj 被垃圾回收,缓存的数据也会被清除

额外的数据

假如我们正在处理一个属于另一个代码的一个对象,也可能是第三方库,并存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡。这时候 WeakMap 正是我们所需要的利器。

我们将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除。

weakMap.set(person, "string");
// 如果person 消失了,string将会被自动清除

来看看例子:

我们有用于处理用户访问计数的代码。收集到的信息被存储在 map 中:一个用户对象作为键,其访问次数为值,当一个用户离开时(该用户对象将被垃圾回收机制回收),这是我们就不再需要它的访问次数了。

// 使用Map的计数函数
// visitsCount.js
let visitsCountMap = new Map(); // map:user =>visits count

function countUser(user{
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count++);
}

let person = { name"suuny" };
countUser(person); // count his visits

// 之后,person 离开了
person = null;

现在 person 这个对象应该被垃圾回收,但它仍在内存中,因为它是 visitsCountMap 中的一个键。当我们移除用户时,我们需要清理 visitsCountMap,否则它将在内存中无限增大。在这复杂的架构中,这种清理会成为一项繁重的任务。我们可以通过使用 WeakMap 来避免这样的问题。

// visitsCount.js
let visitsCountMap = new WeakMap(); // weakMap:user => visits count

function countUser(user);
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user,count+1);

现在我们不需要去清理 visitsCountMap 了。当 person 对象变成不可访问时,即便它是 WeakMap 里的一个键,它也会连同它作为 WeakMap 里的键所对应的信息一同被从内存中删除。

WeakMap WeakSet

我们知道 JS 引擎有垃圾回收机制,在值可访问(并且可能被使用)时将其存储在内存中。

例如:

let person = { name"sunny" };
// 该对象能被访问,person是它的引用
person = null;
// 该对象将会被从内存中清除

通常,当对象、数组这类数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都是可以访问的。如果把一个对象放在数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。

let person = { name"suuny" };
let arr = [person];
person = null//覆盖引用
// 前面由 person 所引用的那个对象被存储在了 array 中
// 所以它不会被垃圾回收机制回收

类似的,如果我们使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也就存在。它会占用内存,并且不会被垃圾回收机制回收。

let person = { name"suuny" };
let map = new Map();
map.set(person, "....");
person = null// 覆盖引用
// person存储在了map中
// 可以使用map.keys 来获取它

WeakMap 在这方面有着根本上的不同。它不会阻止垃圾回收机制作为键的对象的回收。

WeakMap

WeakMapMap 的第一个不同点就是:

WeakMap 得键必须是对象,不能是原始值:

let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "name"); // 以对象为键
// 不能以字符串作为键
weakMap.set("string""name"); // Error,因为 "test" 不是一个对象

现在,如果我们在 WeakMap 中使用一个对象作为键,并且没有其他对这个对象的引用----该对象将会被从内存(和 map)中自动清除。

let person = { name"suuny" };
let weakMap = new WeakMap();
weakMap.set(person, "...");
person = null// 覆盖引用
// person 被从内存中删除了

与上面常规的 Map 的例子相比,现在如果 person 仅仅是作为 WeakMap 的键而存在----它将会被从 map(和内存)中自动删除。

WeakMap 不支持迭代以及 keys()values()entries()方法。所以没有办法获取 WeakMap 的所有键或值。

WeakMap 只有以下方法:

weakMap.get(key);

weakMap.set(key,value);

weakMap.delete(key);

weakMap.has(key);

为什么会有这种限制呢?

这是技术的原因。如果一个对象丢失了其它所有引用(就像上面示例中的 person),那么它就会被垃圾回收机制自动回收。但是在从技术的角度并不能准确知道何时会被回收。

这些都是由 JavaScript 引擎决定的。JavaScript 引擎可能会选择立即执行内存清理,如果现在正在发生很多删除操作,那么 JavaScript 引擎可能就会选择等一等,稍后再进行内存清理。

因此,从技术上讲,WeakMap的当前元素的数量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能没清理,也可能清理了一部分。因此,暂不支持访问 WeakMap 的所有键/值的方法。

WeakSet

WeakSet 的表现类似:

Set 类似,但是我们只能向 WeakSet 添加对象(而不能是原始值)。

对象只有在其它某个(些)地方能被访问的时候,才能留在 Set 中。

Set 一样,WeakSet 支持 addhasdelete 方法,但不支持 sizekeys()等,且不可迭代。

变弱(weak)的同时,它也可以作为额外的存储空间。但并非针对任意数据,而是针对是/否的事实。WeakSet 的元素可能代表着有关该对象的某些信息。

例如,我们可以将用户添加到 WeakSet 中,以追踪访问过我们网站的用户:

let visitedSet = new WeakSet();

let p1 = { name"p1" };
let p2 = { name"p2" };
let p3 = { name"p3" };

visitedSet.add(p1); //p1 访问了我们
visitedSet.add(p2); // 然后是 p2
visitedSet.add(p1); // p1 再次访问

// visitedSet 现在有两个用户了
// 检查 p1 是否来访过?
alert(visitedSet.has(p1)); // true

// 检查 p3 是否来访过?
alert(visitedSet.has(p3)); // false

p1 = null;
// visitedSet 将被自动清理

WeakMapWeakSet 最明显的局限性就是不能迭代,并且无法获取所有当前内容。那样可能会造成不便,但是并不会阻止 WeakMap/WeakSet 完成其主要工作---成为在其它地方管理存储额外的对象数据。

总结

WeakMap 是类似于 Map 的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问它们,便会将它们与其关联值一同删除。

WeakSet 是类似于 Set 的集合,它仅存储对象,并且一旦通过其他方式无法访问它们,便会将其删除。

它们都不支持引用所有键或其计数的方法和属性,仅允许单个操作。

WeakMapWeakSet 被用作主要对象存储之外的辅助数据结构。一旦将对象从主存储器中删除,如果该对象仅被用作 WeakMapWeakSet 的键,那么它将被自动清除。

所有《每日一题》的 知识大纲索引脑图 整理在此:https://www.yuque.com/dfe_evernote/interview/everyday
你也可以点击文末的 “阅读原文” 快速跳转


END
愿你历尽千帆,归来仍是少年。


浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报