作者:Frank Joseph
原文链接:Understanding Weak Reference In JavaScript
译者:Yodonicc
在这篇文章中,Frank Joseph解释了JavaScript中的弱引用和强引用,以及可达性的概念。让我们深入了解一下!
内存和性能管理是软件开发的重要方面,也是每个软件开发者都应该注意的。尽管很有用,但弱引用在JavaScript中并不经常使用。WeakSet和WeakMap是在ES6版本中被引入JavaScript的。
弱引用
澄清一下,与强引用不同,弱引用不会阻止被引用的对象被垃圾回收器回收,即使它是内存中对该对象的唯一引用。
在进入强引用、WeakSet
、Set
、WeakMap
和Map
介绍之前,让我们用下面的片段来说明弱引用。
// 创建一个WeakMap对象的实例。
let human = new WeakMap():
// 创建一个对象,并把它赋给一个叫做man的变量。
let man = { name: "Joe Doe" };
// 对human调用set方法,并向其传递两个参数(key和value)。
human.set(man, "done")
console.log(human)
上面代码的输出将是以下内容。
WeakMap {{…} => 'done'}
man = null;
console.log(human)
man
参数现在被设置为WeakMap
对象。在我们将man
变量重新赋值为null
的时候,内存中对原始对象的唯一引用是弱引用,它来自我们之前创建的WeakMap
。当JavaScript引擎运行一个垃圾回收过程时,man
对象将从内存和我们分配给它的WeakMap
中删除。这是因为它是一个弱引用,并不能阻止垃圾回收。
看起来我们正在取得进展。让我们来谈谈强引用,然后我们将把一切联系起来。
强引用
JavaScript中的强引用是一种防止对象被垃圾回收的引用。它将对象保留在内存中。
下面的代码片断说明了强引用的概念。
let man = {name: "Joe Doe"};
let human = [man];
man = null;
console.log(human);
上面的代码的结果将是这样的。
// 一个长度为1的对象数组。
[{...}]
由于人的数组和对象之间存在强引用,所以不能再通过man
的变量来访问该对象。该对象被保留在内存中,可以通过以下代码进行访问。
console.log(human[0])
这里需要注意的是,弱引用并不能阻止一个对象被垃圾回收,而强引用可以阻止一个对象被垃圾回收。
JavaScript中的垃圾回收
和每一种编程语言一样,内存管理是编写JavaScript时需要考虑的一个关键因素。与C语言不同,JavaScript是一种高级编程语言,在创建对象时自动分配内存,不再需要对象时自动清除内存。当对象不再被使用时清除内存的过程被称为垃圾回收。在谈论JavaScript中的垃圾回收时,几乎不可能不涉及到可达性的概念。
可达性(REACHABILITY)
在一个特定的作用域中的所有值,或者在一个作用域中正在使用的值,在该作用域中被称为 “可达”,并被称为 “可达值”。可达的值总是存储在内存中。
如果是这样的值就被认为是可达的:
- 程序根部的值或从根部引用的值,如全局变量或当前执行的函数、其上下文和回调。
- 通过引用或引用链可以从根部访问的值(例如,全局变量中的一个对象引用了另一个对象,而后者也引用了另一个对象——这些都被认为是可达值)。
下面的代码片断说明了可达性的概念。
let languages = {name: “JavaScript”};
这里我们有一个对象,它有一个键值对(名称为JavaScript
),引用全局变量languages
。如果我们通过给languages
分配null
来覆盖它的值…
languages = null;
…那么这个对象就会被垃圾回收,而JavaScript的值就不能再被访问。下面是另一个例子。
let languages = {name: “JavaScript”};
let programmer = languages;
从上面的代码片断来看,我们可以从languages
变量和programmer
变量中访问对象属性。然而,如果我们把languages设置为null…
languages = null;
…那么该对象将仍然在内存中,因为它可以通过programmer
变量访问。简而言之,这就是垃圾回收的工作方式。
注意:默认情况下,JavaScript的引用使用强引用。要在JavaScript中实现弱引用,你需要使用WeakMap
、WeakSet
或者WeakRef
。
比较Set和WeakSet
一个集合对象是一个唯一值的集合,只有一次出现的机会。一个集合,像一个数组一样,没有键值对。我们可以用数组方法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
s使用弱引用。以下是WeakSet()
的属性:
- 它可能只包含对象。
- 集内的对象可以在其他地方到达。
- 它不能被循环使用。
- 像
Set()
一样,WeakSet()
有add
,has
, 和delete
的方法。
下面的代码说明了如何使用WeakSet()
和一些可用的方法。
const human = new WeakSet();
let paul = {name: "Paul"};
let mary = {gender: "Mary"};
// 把名字为paul的人加入到教室中.
const classroom = human.add(paul);
console.log(classroom.has(paul)); // true
paul = null;
// 教室将自动清理掉人类paul.
console.log(classroom.has(paul)); // false
在第1行,我们创建了一个WeakSet()
的实例。在第3行和第4行,我们创建了对象并把它们分配给各自的变量。在第7行,我们将paul
添加到WeakSet()
中,并将其分配到classroom
变量中。在第11行,我们将paul
的引用变为null
。第15行的代码返回false
,因为WeakSet()
将被自动清理;所以,WeakSet()
不会阻止垃圾回收。
比较Map和WeakMap
正如我们在上面关于垃圾回收的章节中所知道的,只要一个值是可达的,JavaScript引擎就会把它保留在内存中。让我们用一些片段来说明这一点。
let smashing = {name: "magazine"};
// 可以从引用中访问该对象.
// 重新赋值引用的 smashing.
smashing = null;
// 该对象不能再被访问.
当数据结构在内存中时,数据结构的属性被认为是可达的,而且它们通常被保存在内存中。如果我们将一个对象存储在一个数组中,那么只要数组在内存中,即使该对象没有其他的引用,仍然可以被访问。
let smashing = {name: "magazine"};
let arr = [smashing];
// 重写引用.
smashing = null;
console.log(array[0]) // {name: 'magazine'}
即使引用被覆盖了,我们仍然能够访问这个对象,因为这个对象被保存在数组中;因此,只要数组还在内存中,它就被保存在内存中。因此,它没有被垃圾回收。由于我们在上面的例子中使用了数组,我们也可以使用map
。当map
仍然存在时,存储在其中的值就不会被垃圾回收了。
let map = new Map();
let smashing {name: "magazine"};
map.set(smashing, "blog");
// 重写引用.
smashing = null;
// 访问该对象.
console.log(map.keys());
像一个对象一样,map
可以保存键值对,我们可以通过键来访问值。但是对于map
,我们必须使用.get()
方法来访问值。
根据Mozilla开发者网络的说法,Map
对象持有键值对,并记住键的原始插入顺序。任何值(包括对象和原始值)都可以作为键或值使用。
与map
不同的是,WeakMap
持有一个弱引用;因此,如果这些值在其他地方没有被强引用,它就不能阻止垃圾回收删除它所引用的值。除此以外,WeakMap
和map
是一样的。由于弱引用,WeakMaps
是不可枚举的。
对于WeakMap
,键必须是对象,而值可以是数字或字符串。
下面的片段说明了WeakMap的工作原理和其中的方法。
// 创建一个weakMap。
let weakMap = new WeakMap();
let weakMap2 = new WeakMap();
// 创建一个对象。
let ob = {};
// 使用设置方法。
weakMap.set(ob, "Done");
//可以将该值设置为一个对象,甚至是一个函数。
weakMap.set(ob, ob)
// 您可以将值设置为未定义。
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
中使用对象作为键且没有其他引用的一个副作用是,在垃圾回收时它们会被自动从内存中删除。
WeakMap的应用范围
WeakMap
可以用于Web开发的两个领域:缓存和附加数据存储。
缓存
这是一种网络技术,包括保存(即存储)一个给定资源的副本,并在请求时将其送回。一个函数的结果可以被缓存,这样,每当函数被调用时,缓存的结果就可以被重新使用。
让我们来看看这个例子。创建一个文件,命名为cachedResult.js
,并在其中写入以下内容。
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: "Frank"};
let resultSaved = keep(obj)
obj = null;
// console.log(cachedResult.size); 用map可以,用WeakMap不行。
如果我们在上面的代码中使用了Map()
而不是WeakMap()
,并且对函数keep()
进行了多次调用,那么它只会在第一次调用时计算出结果,而在其他时候则会从cachedResult
中获取结果。其副作用是,只要不需要这个对象,我们就需要清理cachedResult
。有了WeakMap()
,一旦对象被垃圾回收,缓存的结果就会自动从内存中删除。缓存是提高软件性能的一个很好的手段——它可以节省数据库使用、第三方API调用和服务器到服务器请求的成本。通过缓存,一个请求的结果的副本被保存在本地。
附加数据存储
WeakMap()
的另一个重要用途是额外的数据存储。想象一下,我们正在建立一个电子商务平台,我们有一个统计访问者的程序,我们希望能够在访问者离开时减少计数。这个任务用Map
来说要求很高,但用WeakMap()
就很容易实现。
let visitorCount = new WeakMap();
function countCustomer(customer){
let count = visitorCount.get(customer) || 0;
visitorCount.set(customer, count + 1);
}
让我们为这个例子创建客户端代码。
let person = {name: "Frank"};
// 对访问的人进行计数。
countCustomer(person)
// 人离开。
person = null。
使用Map()
,每当有客户离开,我们就必须清理visitorCount
;否则,它将在内存中无限增长,占用空间。但是使用WeakMap()
,我们不需要清理visitorCount
;只要一个人(对象)变得不可达,它就会被自动收集垃圾。
结语
在这篇文章中,我们了解了弱引用、强引用和可达性的概念,并试图尽可能地将它们与内存管理联系起来。我希望你能发现这篇文章的价值。请随时发表评论。
注:特别感谢技术指导dazhao(赵达)对本文翻译的审阅指正。