JavaScript 的弱者:第一部分 - WeakMap
你好,2018!距离我们第一次接触 JavaScript 2015(也就是 ES6)已经过去三年了。这段时间里,我们大多数人都关注着一些外观上的变化,比如箭头=>
函数,或者花哨的析构运算符…
。
每个人都需要一些令人兴奋的东西,比如即将推出的花哨|>
管道运算符。谁在乎 ES6 是否也添加了诸如WeakMap
、WeakSet
、Iterables
或Map
之类的东西呢Set
?就连看着这个叫 的东西WeakMap
都感觉很压抑😞。
抛开讽刺,我们来谈谈WeakMaps
💃。
为什么你需要一些弱的东西
我必须承认,这个名字WeakMap
确实用词不当。如果是我,我会把它命名为 SuperMap。在深入讨论定义之前,让我们先花点时间理解一下为什么我们WeakMap
的应用程序中需要 。
想象一下现在是 1990 年🏡,您创建了一个包含当时所有国家🎌的应用程序。
var USSR = {
name: 'Soviet Union',
capital: 'Moscow',
..
..
}
var countries = [ Afganishtan, Albania, Algeria, ..., USSR, ..., Zimbabwe ]
用户可以点击任意国家/地区,获取详细信息,包括该国/地区的面积。以下是一个假设的面积计算函数。
async function calcArea(country) {
const boundaries = await fetch(country);
area = calculateArea(country, boundaries); // takes a long time
return area;
}
缓存区域
每次用户点击一个国家/地区时,您都会计算其面积。但是我们遇到了一个问题!如果用户多次点击一个国家/地区,您就必须重复进行这种庞大的异步计算,而这恰恰是我们应该完全避免的。通常有两种方法可以解决此类问题。
- 防抖功能
- 缓存函数
去抖动是一种平和的方法,可以平息短时间内多次的激进调用。(想象一下,一个不耐烦的用户多次点击刷新按钮)。去抖动允许我们只接受最后一次调用,并丢弃其余调用。
由于国家/地区不会经常改变区域,因此我们可以简单地缓存结果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
太棒了!我们做出了一些重大的性能改进。
但我们还有一个问题,USSR
刚刚分裂成15个新国家。这意味着我们要移除苏联,并将新成立的国家添加到我们的countries
数组中。
countries.remove(USSR);
// add the new countries
countries.add([Armenia, Azerbaijan, ...., Uzbekistan]);
USSR
仅仅从数组中删除并没有什么用,因为我们的缓存仍然包含USSR
计算出的面积。一个比较简单的解决方案是对我们的cachify
函数进行猴子补丁,删除 USSR,但如果世界继续分裂成更小的国家,我们就会面临内存泄漏。
我们需要一种智能且可扩展的方法来清理缓存。其他开发者可以通过多种方式来解决这个问题:
- 维护预先计算的区域数组并使其与国家保持同步。
- 找出一些智能缓存驱逐方法,如 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
现在让USSR
我们来看看这 15 个国家。我们只需要删除USSR
应用中所有指向该 obj 的引用,我们的cachedCalcArea
函数就会自动忽略USSR
缓存中的条目。这样就避免了内存泄漏!
它怎么会忘记事情呢?
WeakMap
工作原理与常规 Map 类似,Map
但为了成为 Map 的遗忘版本,它施加了以下约束:
- 不允许使用原始数据类型键(数字、字符串、null、true 等)
- 你不能列出WeakMap 中的所有值
让我们看一个 WeakMap 的假设例子
- 想象一个
WeakMap
实例是一座拥有数千扇🚪门的建筑物。
var building = new WeakMap();
- 每扇门都有一把独一无二的钥匙,我们自己也拥有一把钥匙🔑
🚪101
。由于上述限制,钥匙只能是一个对象。
var key = {
password: '🔑'
};
- 我们可以用这把钥匙锁/开门。
building.set(key, '🚪101');
building.get(key); // 🚪101
- 现在,小偷看到了我们的钥匙(这是 Javascript!)并且他试图伪造一把复制钥匙。
var fake_key = {
password: '🔑'
};
- 因为我们生活在 Javascript 世界中,所以我们清楚地知道,尽管它们看起来相同,但实际上并不相同
equal
。
fake_key === key // false
- 我们的小偷没有读到这篇精彩的文章,他试图用假钥匙进入我们的大楼,但失败了:(。
building.get(fake_key); // undefined
如果我们丢失了钥匙会发生什么
只要某个变量保存着我们原始密钥的引用,我们就是安全的。但是,如果有一天,整个应用程序中没有一个变量保存着我们密钥的引用,我们就会失去对 的访问权限🚪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
完成上述步骤后,您可以看到,在应用程序的当前状态下,无法访问 USSR 对象。JavaScript 垃圾回收器会根据这一情况自动清除为 USSR 区域保留的内存。请注意,删除操作是在后台进行的,我们所做的只是将其替换Map
为WeakMap
。是不是非常强大?
WeakMap 要点
- 记住不要改变关键对象,因为在 Javascript 中即使你改变对象,对象引用仍然保持不变。
var obj = {name: '🐕'};
weakMap.set(obj, 'animal');
obj.name = '🙍♂️';
weakMap.get(obj); // 'animal'
- WeakMap 无法接受JavaScript 原始值作为键
Map
。如果您想使用它们作为键,则应使用 WeakMap 。
weakMap.set('key', 'value'); // Error!
- 有时不缓存函数反而会更快。如果你的函数执行时间仅为一毫秒,那么缓存反而会降低它的速度。
- 你可以使用任何东西作为
value
forWeakMap
/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))
我真心希望本文能帮助您理解WeakMaps
。我喜欢将它与诸如Immutable.js
或 之类的库一起使用Redux
,因为它们强制了不可变性。即使您不使用这些库,只要您不改变对象,您仍然可以从 WeakMap 中获益。
我计划写一篇Part-2
关于 Javascript Underdogs 的文章,请在评论中告诉我你认为哪个 Javascript 特性很神奇但被低估了。
如果您❤️这篇文章,请分享这篇文章来传播。
鏂囩珷鏉ユ簮锛�https://dev.to/kepta/javascript-underdogs-part-1---the-weakmap-4jih