深入浅出 JavaScript 弱引用
内存和性能管理是软件开发的重要方面,也是每个软件开发人员都应该注意的方面。虽然弱引用很有用,但在 JavaScript 中并不经常使用。在 ES6 版本中,JavaScript 引入了 WeakSet
和 WeakMap
。
1. 弱引用
与强引用不同,弱引用并不阻止被引用的对象被垃圾收集器回收或收集,即使它是内存中对对象的唯一引用。
在讨论强引用、WeakSet
、Set
、WeakMap
和 Map
之前,让我们用下面的代码片段来演示弱引用:
// 创建 WeakMap 对象的实例 let human = new WeakMap(); // 创建一个对象,并将其赋值给名为 man 的变量 let man = { name: "xiaan" }; // 调用 human 的 set 方法,并传递两个参数(键和值)给它 human.set(man, "done") console.log(human)
以上代码的输出如下:
WeakMap {{…} => 'done'} man = null; console.log(human)
当我们将 man
变量重新赋值为 null
时,内存中对原始对象的唯一引用是弱引用,它来自我们前面创建的 WeakMap
。当 JavaScript 引擎运行垃圾收集过程时,man
对象将从内存和我们分配给它的 WeakMap
中删除。这是因为它是一个弱引用,并且它不阻止垃圾收集。接下来我们谈谈强引用。
2. 强引用
JavaScript 中的强引用是防止对象被垃圾回收的引用。它将对象保存在内存中。
下面的代码片段说明了强引用的概念:
let man = {name: "xiaan"}; let human = [man]; man = null; console.log(human);
以上代码的结果如下:
// 长度为 1 的对象数组 [{…}]
由于 human
数组和对象之间存在强引用。对象被保留在内存中,可以通过以下代码访问:
console.log(human[0])
这里要注意的重要一点是,弱引用不会阻止对象被垃圾回收,而强引用却会阻止对象被垃圾回收。
3. JavaScript 的垃圾收集
与所有编程语言一样,在编写 JavaScript 时,内存管理是需要考虑的关键因素。与 C 语言不同,JavaScript 是一种高级编程语言,它在创建对象时自动分配内存,在不再需要对象时自动清除内存。当不再使用对象时清除内存的过程称为垃圾收集。在谈论 JavaScript 中的垃圾收集时,几乎不可能不触及「可达性」的概念。
3.1 可达性
在特定作用域中的所有值或在作用域中使用的所有值都被称为在该作用域中的“可达”,并被称为“可达值”。可访问的值总是存储在内存中。
在以下情况下,值被认为是可达的:
- 程序根中的值或从根中引用的值,如全局变量或当前执行的函数、它的上下文和回调。
- 通过引用或引用链从根中访问的值(例如,全局变量中的对象引用另一个对象,该对象也引用另一个对象——这些都被认为是可访问的值)。
下面的代码片段说明了可达性的概念:
var person = {name: "xiaan"};
这里我们有一个对象,它的键值对(name
为 "xiaan"
)引用全局变量 person
。如果我们通过赋值 null
来覆盖 person
的值:
person = null;
那么对象将被垃圾回收,"xiaan"
值将无法再次访问。下面是另一个例子:
var person = {name: "xiaan"}; var programmer = person;
从上面的代码片段中,我们可以从 person
变量和 programmer
变量访问 object 属性。然而,如果我们将person
设置为 null
:
person = null;
那么对象仍然在内存中,因为它可以通过 programmer
变量访问。简单地说,这就是垃圾收集的工作方式。
注意:默认情况下,JavaScript 对其引用使用强引用。要在 JavaScript 中实现弱引用,可以使用 WeakMap
、WeakSet
或 WeakRef
。
4. Set VS WeakSet
set
对象是一个只有一次出现的唯一值的集合。像数组一样,集合没有键值对。我们可以使用for...of
和 .forEach
的数组方法遍历。
让我们用以下片段来说明这一点:
let setArray = new Set(["Joseph", "Frank", "John", "Davies"]); for (let names of setArray){ console.log(names) }// Joseph Frank John Davies
我们也可以使用 .forEach
遍历:
setArray.forEach((name, nameAgain, setArray) =>{ console.log(names); });
WeakSet
是唯一对象的集合。正如其名称一样,弱集使用弱引用。以下是 WeakSet()
的特性:
- 它可能只包含对象。
- 集合中的对象可以在其他地方访问。
- 它不能循环遍历。
- 像
Set()
一样,WeakSet()
有add
、has
和delete
方法。
下面的代码说明了如何使用 WeakSet()
和一些可用的方法:
const human = new WeakSet(); let person = {name: "xiaan"}; human.add(person); console.log(human.has(person)); // true person = null; console.log(human.has(person)); // false
在第 1 行,我们创建了 WeakSet()
的一个实例。在第 3 行,我们创建了对象并将它分配给变量 person
。在第 5 行,我们将 person
添加到 WeakSet()
中。在第 9 行,我们将 person
引用设为空。第 11 行代码返回false
,因为 WeakSet()
将被自动清除,因此,WeakSet()
不会阻止垃圾回收。
5. Map VS WeakMap
我们从上面关于垃圾收集的部分了解到,只要可以访问,JavaScript 引擎就会在内存中保留一个值。让我们用一些片段来说明这一点:
let person = {name: "xiaan"}; // 对象可以从引用中访问 // 覆盖引用 person. person = null; // 该对象不能被访问
当数据结构在内存中时,数据结构的属性被认为是可访问的,并且它们通常保存在内存中。如果将对象存储在数组中,那么只要数组在内存中,即使没有其他引用,也仍然可以访问对象。
let person = {name: "xiaan"}; let arr = [person]; // 覆盖引用 person = null; console.log(array[0]) // {name: 'xiaan'}
即使引用被覆盖,我们仍然能够访问这个对象因为对象被保存在数组中。因此,只要数组仍然在内存中,它就保存在内存中。因此,它没有被垃圾回收。由于我们在上面的例子中使用了数组,我们也可以使用 map
。当 map
仍然存在时,存储在其中的值将不会被垃圾回收。
let map = new Map(); let person = {name: "xiaan"}; map.set(person, "person"); // 覆盖引用 person = null; // 还能访问对象 console.log(map.keys());
与对象一样,map
可以保存键—值对,我们可以通过键访问值。但是对于 map
,我们必须使用 .get()
方法来访问值。
根据 Mozilla Developer Network,Map
对象保存键—值对并记住键的原始插入顺序。任何值(包括对象值和原语值)都可以用作键或值。
与 map
不同,WeakMap
保存弱引用。因此,如果这些值在其他地方没有被强引用,它不会阻止垃圾回收删除它引用的值。除此之外,WeakMap
与 map
是相同的。由于弱引用,WeakMap
不可枚举。
对于 WeakMap
,键必须是对象,值可以是数字或字符串。
下面的代码片段说明了 WeakMap
的工作原理和其中的方法:
// 创建一个 WeakMap let weakMap = new WeakMap(); let weakMap2 = new WeakMap(); // 创建一个对象 let ob = {}; // 使用 set 方法 weakMap.set(ob, "Done"); // 你可以将值设置为一个对象甚至一个函数 weakMap.set(ob, ob) // 可以设置为undefined weakMap.set(ob, undefined); // WeakMap 也可以是值和键 weakMap.set(weakMap2, weakMap) // 要获取值,使用 get 方法 weakMap.get(ob) // Done // 使用 has 方法 weakMap.has(ob) // true weakMap.delete(ob) weakMap.has(ob) // false
在没有其他引用的 WeakMap
中使用对象作为键的一个主要副作用是,它们将在垃圾收集期间自动从内存中删除。
6.「WeakMap 的应用」
WeakMap
可以用于 web 开发的两个领域:缓存和额外的数据存储。
6.1 缓存
这是一种 web 技术,它涉及到保存(即存储)给定资源的副本,并在请求时返回它。可以缓存函数的结果,以便在调用函数时重用缓存的结果。
让我们来看看实际情况。
let cachedResult = new WeakMap(); // 存储结果的函数 function keep(obj){ if(!cachedResult.has(obj){ let result = obj; cachedResult.set(obj, result); } return cachedResult.get(obj); } let obj = {name: "xiaan"}; let resultSaved = keep(obj) obj = null; // console.log(cachedResult.size); Possible with map, not with WeakMap
如果我们在上面的代码中使用 Map()
而不是 WeakMap()
,并且对 keep()
函数有多次调用,那么它只会在第一次调用时计算结果,并在其他时候从 cachedResult
检索结果。副作用是,每当对象不需要时,我们就需要清理 cachedResult
。使用 WeakMap()
,一旦对象被垃圾回收,缓存的结果就会自动从内存中删除。缓存是提高软件性能的一种很好的方法——它可以节省数据库使用、第三方 API 调用和服务器对服务器请求的成本。通过缓存,请求结果的副本被保存在本地。
6.2 额外的数据存储
WeakMap()
的另一个重要用途是额外的数据存储。想象一下,我们正在建立一个电子商务平台,我们有一个计算访客数量的程序,我们希望能够在访客离开时减少计数。这个任务在 Map
中要求很高,但在 WeakMap()
中很容易实现:
let visitorCount = new WeakMap(); function countCustomer(customer){ let count = visitorCount.get(customer) || 0; visitorCount.set(customer, count + 1); } let person = {name: "xiaan"}; // 统计访问人数 countCustomer(person) // 访客离开 person = null;
使用 Map()
,我们必须在客户离开时清除 visitorCount
,否则,它将在内存中无限增长,占用空间。但是使用 WeakMap()
,我们不需要清理 visitorCount
,一旦一个人(对象)变得不可访问,它就会自动被垃圾回收。
7. 小结
在本文中,我们了解了弱引用、强引用和可达性的概念,并尽可能地将它们与内存管理联系起来。