正如我们在 垃圾回收 一章中所了解的,JavaScript 引擎在值“可达”并且可能被使用时会将其保存在内存中。
例如
let john = { name: "John" };
// the object can be accessed, john is the reference to it
// overwrite the reference
john = null;
// the object will be removed from memory
通常,对象属性或数组或其他数据结构的元素被认为是可达的,并且在该数据结构存在于内存中时会保存在内存中。
例如,如果我们将一个对象放入数组中,那么在数组存在时,该对象也将存在,即使没有其他引用指向它。
就像这样
let john = { name: "John" };
let array = [ john ];
john = null; // overwrite the reference
// the object previously referenced by john is stored inside the array
// therefore it won't be garbage-collected
// we can get it as array[0]
与此类似,如果我们在常规 Map
中将对象用作键,那么在 Map
存在时,该对象也存在。它占用内存,并且可能不会被垃圾回收。
例如
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // overwrite the reference
// john is stored inside the map,
// we can get it by using map.keys()
WeakMap
在这方面有根本的不同。它不会阻止垃圾回收键对象。
让我们看看示例中的含义。
WeakMap
Map
和 WeakMap
之间的第一个区别是键必须是对象,而不是原始值
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok"); // works fine (object key)
// can't use a string as the key
weakMap.set("test", "Whoops"); // Error, because "test" is not an object
现在,如果我们使用一个对象作为其中的键,并且没有其他对该对象的引用,它将自动从内存(和映射)中删除。
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // overwrite the reference
// john is removed from memory!
将其与上面常规 Map
示例进行比较。现在,如果 john
仅作为 WeakMap
的键存在,它将自动从映射(和内存)中删除。
WeakMap
不支持迭代和方法 keys()
、values()
、entries()
,因此无法从中获取所有键或值。
WeakMap
仅具有以下方法
为什么会有这样的限制?这是出于技术原因。如果一个对象丢失了所有其他引用(如上面代码中的 john
),那么它将被自动垃圾回收。但在技术上,它并没有确切地指定何时进行清理。
JavaScript 引擎决定这一点。它可以选择立即执行内存清理,或等待并在以后发生更多删除时进行清理。因此,在技术上,WeakMap
的当前元素计数是未知的。引擎可能已经清理了它,也可能没有,或者只是部分清理了它。因此,不支持访问所有键/值的那些方法。
现在,我们哪里需要这样的数据结构?
用例:附加数据
WeakMap
的主要应用领域是附加数据存储。
如果我们使用“属于”其他代码(甚至可能是第三方库)的对象,并且希望存储一些与之关联的数据,这些数据只应在对象存活时存在,那么 WeakMap
正是所需。
我们使用对象作为键将数据放入 WeakMap
中,当对象被垃圾回收时,该数据也会自动消失。
weakMap.set(john, "secret documents");
// if john dies, secret documents will be destroyed automatically
我们来看一个示例。
例如,我们有用于记录用户访问次数的代码。信息存储在地图中:用户对象是键,访问次数是值。当用户离开(其对象被垃圾回收)时,我们不再希望存储其访问次数。
以下是使用 Map
的计数函数示例
// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
// increase the visits count
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
以下是代码的另一部分,可能是使用它的另一个文件
// 📁 main.js
let john = { name: "John" };
countUser(john); // count his visits
// later john leaves us
john = null;
现在,john
对象应被垃圾回收,但仍保留在内存中,因为它是 visitsCountMap
中的键。
当我们删除用户时,我们需要清理 visitsCountMap
,否则它将在内存中无限增长。在复杂架构中,这种清理可能成为一项繁琐的任务。
我们可以通过改为使用 WeakMap
来避免这种情况
// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// increase the visits count
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
现在我们不必清理 visitsCountMap
。在 john
对象变得不可达(除了作为 WeakMap
的键之外)之后,它将从内存中删除,同时也会从 WeakMap
中删除该键的信息。
用例:缓存
另一个常见的示例是缓存。我们可以存储(“缓存”)函数的结果,以便对同一对象的未来调用可以重用它。
为了实现这一点,我们可以使用 Map
(不是最佳方案)
// 📁 cache.js
let cache = new Map();
// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculations of the result for */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// Now we use process() in another file:
// 📁 main.js
let obj = {/* let's say we have an object */};
let result1 = process(obj); // calculated
// ...later, from another place of the code...
let result2 = process(obj); // remembered result taken from cache
// ...later, when the object is not needed any more:
obj = null;
alert(cache.size); // 1 (Ouch! The object is still in cache, taking memory!)
对于使用同一对象的 process(obj)
的多次调用,它只在第一次计算结果,然后从 cache
中获取结果。缺点是我们需要在不再需要对象时清理 cache
。
如果我们用 WeakMap
替换 Map
,那么这个问题就会消失。在对象被垃圾回收后,缓存的结果将自动从内存中删除。
// 📁 cache.js
let cache = new WeakMap();
// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculate the result for */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* some object */};
let result1 = process(obj);
let result2 = process(obj);
// ...later, when the object is not needed any more:
obj = null;
// Can't get cache.size, as it's a WeakMap,
// but it's 0 or soon be 0
// When obj gets garbage collected, cached data will be removed as well
WeakSet
WeakSet
的行为类似
- 它类似于
Set
,但我们只能向WeakSet
添加对象(而不是基元)。 - 对象在从其他地方可达时存在于集合中。
- 与
Set
一样,它支持add
、has
和delete
,但不支持size
、keys()
和任何迭代。
由于“弱”,它还可用作附加存储。但不是用于任意数据,而是用于“是/否”事实。WeakSet
中的成员资格可能表示有关对象的信息。
例如,我们可以将用户添加到 WeakSet
以跟踪访问我们网站的用户
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John visited us
visitedSet.add(pete); // Then Pete
visitedSet.add(john); // John again
// visitedSet has 2 users now
// check if John visited?
alert(visitedSet.has(john)); // true
// check if Mary visited?
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet will be cleaned automatically
WeakMap
和 WeakSet
最明显的限制是缺少迭代,并且无法获取所有当前内容。这可能看起来不方便,但并不会妨碍 WeakMap/WeakSet
执行其主要工作——成为存储在其他位置的对象的“附加”数据存储。
摘要
WeakMap
是类似于 Map
的集合,它只允许对象作为键,并且在它们通过其他方式变得不可访问时将它们与关联的值一起删除。
WeakSet
是类似于 Set
的集合,它只存储对象,并且在它们通过其他方式变得不可访问时将它们删除。
它们的主要优点是它们对对象具有弱引用,因此垃圾回收器可以轻松地删除它们。
代价是不支持 clear
、size
、keys
、values
...
WeakMap
和 WeakSet
除了“主”对象存储之外,还用作“辅助”数据结构。一旦对象从主存储中删除,如果它仅作为 WeakMap
的键或在 WeakSet
中找到,它将自动清除。
评论
<code>
标签,对于多行代码,请将其包装在<pre>
标签中,对于 10 行以上的代码,请使用沙箱(plnkr、jsbin、codepen…)