JavaScript 的弱者:第一部分 - WeakMap

2025-06-09

JavaScript 的弱者:第一部分 - WeakMap

你好,2018!距离我们第一次接触 JavaScript 2015(也就是 ES6)已经过去三年了。这段时间里,我们大多数人都关注着一些外观上的变化,比如箭头=>函数,或者花哨的析构运算符

gif1

每个人都需要一些令人兴奋的东西,比如即将推出的花哨|>管道运算符。谁在乎 ES6 是否也添加了诸如WeakMapWeakSetIterablesMap之类的东西呢Set?就连看着这个叫 的东西WeakMap都感觉很压抑😞。

抛开讽刺,我们来谈谈WeakMaps💃。

为什么你需要一些弱的东西

我必须承认,这个名字WeakMap确实用词不当。如果是我,我会把它命名为 SuperMap。在深入讨论定义之前,让我们先花点时间理解一下为什么我们WeakMap的应用程序中需要 。

想象一下现在是 1990 年🏡,您创建了一个包含当时所有国家🎌的应用程序。

var USSR = {
  name: 'Soviet Union',
  capital: 'Moscow',
  ..
  ..
}

var countries = [ Afganishtan, Albania, Algeria, ..., USSR, ..., Zimbabwe ]
Enter fullscreen mode Exit fullscreen mode

用户可以点击任意国家/地区,获取详细信息,包括该国/地区的面积。以下是一个假设的面积计算函数。

async function calcArea(country) {
  const boundaries = await fetch(country);

  area = calculateArea(country, boundaries); // takes a long time

  return area;
}
Enter fullscreen mode Exit fullscreen mode

缓存区域

每次用户点击一个国家/地区时,您都会计算其面积。但是我们遇到了一个问题!如果用户多次点击一个国家/地区,您就必须重复进行这种庞大的异步计算,而这恰恰是我们应该完全避免的。通常有两种方法可以解决此类问题。

  1. 防抖功能
  2. 缓存函数

去抖动是一种平和的方法,可以平息短时间内多次的激进调用。(想象一下,一个不耐烦的用户多次点击刷新按钮)。去抖动允许我们只接受最后一次调用,并丢弃其余调用。

由于国家/地区不会经常改变区域,因此我们可以简单地缓存结果calcArea

我们可以同时使用缓存去抖动来提高应用程序的性能。下面是一个通用的缓存函数,我们将用它来进行缓存calcArea

function cachify(fn) {
  // its a good idea to hide you cache inside the closure
  var cache = new Map();
  return arg => {
    if (cache.has(arg)) {
      return cache.get(arg);
    }
    var computed = fn(arg);
    cache.set(arg, computed);
    return computed;
  };
}

cachedCalcArea = cachify(calcArea);

cachedCalcArea(USSR); // goes and computes the area
cachedCalcArea(USSR); // already computed, returns the cached area
Enter fullscreen mode Exit fullscreen mode

太棒了!我们做出了一些重大的性能改进。

但我们还有一个问题,USSR刚刚分裂成15个新国家。这意味着我们要移除苏联,并将新成立的国家添加到我们的countries数组中。

countries.remove(USSR);
// add the new countries
countries.add([Armenia, Azerbaijan, ...., Uzbekistan]);
Enter fullscreen mode Exit fullscreen mode

USSR仅仅从数组中删除并没有什么用,因为我们的缓存仍然包含USSR计算出的面积。一个比较简单的解决方案是对我们的cachify函数进行猴子补丁,删除 USSR,但如果世界继续分裂成更小的国家,我们就会面临内存泄漏。

我们需要一种智能且可扩展的方法来清理缓存。其他开发者可以通过多种方式来解决这个问题:

  1. 维护预先计算的区域数组并使其与国家保持同步。
  2. 找出一些智能缓存驱逐方法,如 LRU、基于时间等。

预先计算每个国家的面积似乎是浪费计算,因为大多数用户永远不会看到每个国家。

我们可以使用智能缓存策略,例如“最近最少使用缓存”,这种缓存会自动删除最近最少使用的条目。但是,由于我们拥有 160 多个国家/地区,内存不会耗尽,LRU 似乎并不那么神奇和无缝。

那么 WeakMap 怎么样?

地图

WeakMap是我们缓存问题中缺失的一块拼图。它会自动移除*所有未使用的引用。

WeakMap对象是键/值对的集合,其中键被弱引用。键必须是对象,值可以是任意值。 ” - MDN

我想说 WeakMap 只不过是一个患有痴呆症的普通 Map 。它是一种非常宽容的数据结构,它会忘记那些不再重要的事情。(我们也应该这样 :P)

我们可以简单地在缓存功能中用Map替换。WeakMap

function weakCache(fn) {
  var cache = new WeakMap(); // <-- Behold the Weak!
  return (arg) => {
    if (cache.has(arg)) {
      return cache.get(arg);
    }
    var computed = fn(arg);
    cache.set(arg, computed);
    return computed;
  }
}
cachedCalcArea = weakCache(calcArea);

cachedCalcArea(USSR); // cache miss
cachedCalcArea(USSR); // cache hit
Enter fullscreen mode Exit fullscreen mode

现在让USSR我们来看看这 15 个国家。我们只需要删除USSR应用中所有指向该 obj 的引用,我们的cachedCalcArea函数就会自动忽略USSR缓存中的条目。这样就避免了内存泄漏!

它怎么会忘记事情呢?

WeakMap工作原理与常规 Map 类似,Map但为了成为 Map 的遗忘版本,它施加了以下约束:

  • 不允许使用原始数据类型键(数字、字符串、null、true 等)
  • 你不能列出WeakMap 中的所有值

让我们看一个 WeakMap 的假设例子

  • 想象一个WeakMap实例是一座拥有数千扇🚪门的建筑物。
  var building = new WeakMap();
Enter fullscreen mode Exit fullscreen mode
  • 每扇门都有一把独一无二的钥匙,我们自己也拥有一把钥匙🔑 🚪101。由于上述限制,钥匙只能是一个对象。
  var key = {
    password: '🔑'
  };
Enter fullscreen mode Exit fullscreen mode
  • 我们可以用这把钥匙锁/开门。
  building.set(key, '🚪101');

  building.get(key); // 🚪101
Enter fullscreen mode Exit fullscreen mode
  • 现在,小偷看到了我们的钥匙(这是 Javascript!)并且他试图伪造一把复制钥匙。
  var fake_key = {
    password: '🔑'
  };
Enter fullscreen mode Exit fullscreen mode
  • 因为我们生活在 Javascript 世界中,所以我们清楚地知道,尽管它们看起来相同,但实际上并不相同equal
  fake_key === key // false
Enter fullscreen mode Exit fullscreen mode
  • 我们的小偷没有读到这篇精彩的文章,他试图用假钥匙进入我们的大楼,但失败了:(。
  building.get(fake_key); // undefined
Enter fullscreen mode Exit fullscreen mode

如果我们丢失了钥匙会发生什么

只要某个变量保存着我们原始密钥的引用,我们就是安全的。但是,如果有一天,整个应用程序中没有一个变量保存着我们密钥的引用,我们就会失去对 的访问权限🚪101

这正是智能缓存的强大之处WeakMap。如果我们丢失了键,GC 可以推断出无法访问与该键关联的对象,并且可以安全地将其从内存中删除。

WeakMap注意:这是 a和之间的关键区别Map如果丢失了密钥,WeakMap则会删除,但在 Map 中,您可以简单地列出所有密钥来找到丢失的密钥。<key,value>

回到苏联问题,当苏联分裂为 15 个国家时,我们只需要注意删除应用程序中的所有对苏联对象的引用。

countries.remove(USSR); // remove from array

USSR = undefined; // unset the variable

// at this point there is no way to get the cached area of USSR since it doesn't exist anymore
Enter fullscreen mode Exit fullscreen mode

完成上述步骤后,您可以看到,在应用程序的当前状态下,无法访问 USSR 对象。JavaScript 垃圾回收器会根据这一情况自动清除为 USSR 区域保留的内存。请注意,删除操作是在后台进行的,我们所做的只是将其替换MapWeakMap。是不是非常强大?

WeakMap 要点

  • 记住不要改变关键对象,因为在 Javascript 中即使你改变对象,对象引用仍然保持不变。
var obj = {name: '🐕'};
weakMap.set(obj, 'animal');

obj.name = '🙍‍♂️';
weakMap.get(obj); // 'animal'
Enter fullscreen mode Exit fullscreen mode
  • WeakMap 无法接受JavaScript 原始值作为键Map。如果您想使用它们作为键,则应使用 Wea​​kMap 。
weakMap.set('key', 'value'); // Error!
Enter fullscreen mode Exit fullscreen mode
  • 有时不缓存函数反而会更快。如果你的函数执行时间仅为一毫秒,那么缓存反而会降低它的速度。
  • 你可以使用任何东西作为valuefor WeakMap/ Map。甚至 Promise 也可以!
  • 未被使用的键的驱逐不会立即发生。这取决于垃圾收集器的心情。不过你不必担心这部分。
  • WeakMap 非常适合处理派生状态。很多时候,你的应用程序的状态可以简单地从其他状态派生出来。在下面的示例中,你可以看到使用缓存函数派生值更加易于维护,也更容易理解。
var user = {
    name: "Kushan Joshi"
}

var websites = ['Facebook', 'Github', 'Twitter', 'Dev.to', 'Medium'];

var memberOf = (user) => websites.filter(website => isUser(user));

// save the websites and keep track of it, too complicated 🤮 !
user.memberOf = memberOf(user);

// deriving the value using weakMaps, awesomo 🤖!
cachedMemberOf = weakCache(memberOf); // avoid recomputing everytime
// or derive it everytime you need it
console.log(cachedMemberOf(user)); 
render(cachedMemberOf(user))
Enter fullscreen mode Exit fullscreen mode

我真心希望本文能帮助您理解WeakMaps。我喜欢将它与诸如Immutable.js或 之类的库一起使用Redux,因为它们强制了不可变性。即使您不使用这些库,只要您不改变对象,您仍然可以从 WeakMap 中获益。

我计划写一篇Part-2关于 Javascript Underdogs 的文章,请在评论中告诉我你认为哪个 Javascript 特性很神奇但被低估了。

如果您❤️这篇文章,请分享这篇文章来传播。

通过 Twitter @kushan2020联系我

鏂囩珷鏉ユ簮锛�https://dev.to/kepta/javascript-underdogs-part-1---the-weakmap-4jih
PREV
10 个提升 UI 技能的网站
NEXT
Flask 和 Docker 入门 🐳🚀 flask_starter_app