首页 阅读DAY8 JavaScript高级程序设计 6章下 高级引用类型
文章
取消

阅读DAY8 JavaScript高级程序设计 6章下 高级引用类型

开始阅读JavaScript高级程序设计(第5版)学习JS,总共有1000+页,非常全面,短期看完不太现实,找到了一篇博客,花些时间跟着这篇博客过一下红宝书。

JavaScript高级程序设计(第5版)微信读书

红宝书《JavaScript高级程序设计(第5版)》学习大纲 - 大前端全栈开发 - SegmentFault 思否

object,map,set,weakmap和weakset的基本api对比:

操作ObjectMapSetWeakMapWeakSet
创建{} / new Object()new Map()new Set()new WeakMap()new WeakSet()
添加/设置obj[key] = valset(key, val)add(val)set(key, val)add(val)
获取obj[key]get(key)get(key)
是否存在key in objhas(key)has(val)has(key)has(val)
删除delete obj[key]delete(key)delete(val)delete(key)delete(val)
清空手动遍历删除clear()clear()
长度Object.keys().lengthsizesize
键类型限制字符串/Symbol任意值任意值仅对象仅对象
keys()Object.keys(obj)map.keys()set.keys() (= values)
values()Object.values(obj)map.values()set.values()
entries()Object.entries(obj)map.entries()set.entries()[val,val]
forEach()
for…of
for…in
插入顺序保证❌(整数键升序优先)
垃圾回收强引用强引用强引用弱引用(键)弱引用(值)

Set:

Set作为一种新的引用类型为这门语言带来了集合数据结构。Set在很多方面都像是加强的Map,这是因为它们的大多数API和行为是共有的。

Map 是键值对的集合,而 Set 是值的集合——只存值,不存键,且每个值只能出现一次。这个”不重复”的特性是 Set 存在的核心意义,常用于数组去重、判断元素是否存在等场景。

基本API:

和 Map 一样,用 new Set() 创建空集合,也可以传入可迭代对象做初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 空集合
const s = new Set();

// 用数组初始化
const s1 = new Set(["val1", "val2", "val3"]);
alert(s1.size); // 3

// 用自定义迭代器初始化
const s2 = new Set({
 [Symbol.iterator]: function*() {
 yield "val1";
 yield "val2";
 yield "val3";
 }
});
alert(s2.size); // 3

数组是最常见的初始化方式。自定义迭代器用得少,但说明了 Set 接受任何可迭代对象——跟 Map 的初始化规则一致。

Set 提供五个核心方法,和 Map 的操作一一对应,只是没有”键”的概念:

操作方法说明
添加add(value)添加值,返回集合实例(支持链式调用)
查询has(value)是否包含该值,返回布尔值
大小size元素数量(属性,不是方法)
删除delete(value)删除指定值,返回布尔值表示是否存在
清空clear()删除所有值

完整的使用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const s = new Set();

s.has("Matt"); // false — 还没添加
s.size; // 0

s.add("Matt").add("Frisbie"); // add() 返回集合实例,可以链式调用

s.has("Matt"); // true
s.size; // 2

s.delete("Matt"); // true — 集合中存在,删除成功
s.has("Matt"); // false
s.size; // 1

s.clear(); // 清空所有值
s.has("Frisbie"); // false
s.size; // 0

add() 返回集合实例这个设计特别实用——它意味着可以在初始化时直接链式添加:

1
2
3
const s = new Set().add("val1");
s.add("val2").add("val3");
alert(s.size); // 3

任意类型的值与 SameValueZero:

Set 可以存储任何 JavaScript 数据类型,和 Map 一样使用 SameValueZero 判断值是否相同——基本等同于严格相等 ===,但额外认定 NaN 等于 NaN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const s = new Set();

const functionVal = function() {};
const symbolVal = Symbol();
const objectVal = new Object();

s.add(functionVal);
s.add(symbolVal);
s.add(objectVal);

s.has(functionVal); // true — 同一个实例
s.has(symbolVal); // true
s.has(objectVal); // true

// 新的 function() {} 是新实例,SameValueZero 比较为 false
s.has(function() {}); // false

这里和 Map 的逻辑完全一致:独立的不同实例不会冲突functionValfunction() {} 看起来一样,但后者是新创建的对象,存在内存的不同位置,SameValueZero 比较引用地址,所以判定为不同。

同样,修改对象的内容或属性也不会影响 Set 的判断——Set 存的是引用,引用不变,has() 就能找到:

1
2
3
4
5
6
7
8
9
10
11
12
const s = new Set();
const objVal = {};
const arrVal = [];

s.add(objVal);
s.add(arrVal);

objVal.bar = "bar"; // 修改对象的属性
arrVal.push("bar"); // 修改数组的内容

s.has(objVal); // true — 引用没变
s.has(arrVal); // true — 引用没变

幂等性:重复操作不会有副作用

add()delete() 都是幂等的——重复执行不会产生意外效果:

1
2
3
4
5
6
7
8
9
10
const s = new Set();

s.add("foo");
s.size; // 1

s.add("foo"); // 重复添加同一个值
s.size; // 1 — 集合中只有一个 "foo",不会变两个

s.delete("foo"); // true — 存在,删除成功
s.delete("foo"); // false — 已不存在,删除操作无害地返回 false

这个特性让 Set 的使用更安全——你不需要先检查 has() 再决定是否 add(),直接 add() 就行,重复添加不会出错。delete() 的返回值也很有用:true 表示确实删了东西,false 表示本来就没有,可以据此做逻辑判断。

Set vs Map 操作对比

操作MapSet
添加set(key, value)add(value)
查询get(key) / has(key)has(value)
大小sizesize
删除delete(key)delete(value)
清空clear()clear()
相等判断SameValueZeroSameValueZero
链式调用set() 返回实例add() 返回实例

Set 本质上就是”只有键没有值的 Map”——API 设计、比较逻辑、迭代方式都和 Map 对称。理解了 Map,Set 几乎不需要额外学习成本。

顺序与迭代:

上一节介绍了 Set 的基本操作——添加、查询、删除。但 Set 的能力不止于此,它和 Map 一样维护值插入时的顺序,支持多种方式遍历。理解这些迭代机制,才能在实际开发中选择最合适的遍历方式,也才能理解为什么 [...new Set(arr)] 可以做数组去重。

迭代器方法:三个方法,同一个迭代器

Set 提供了 values()keys()Symbol.iterator 三种方式获取迭代器,但它们的遍历结果完全一样:

1
2
3
4
const s = new Set(["val1", "val2", "val3"]);

s.values() === s[Symbol.iterator](); // true
s.keys() === s[Symbol.iterator](); // true

为什么三个方法指向同一个迭代器?因为 Set 只有值,没有键keys() 的存在纯粹是为了和 Map 的 API 对称——Map 有 keys() 返回键迭代器,Set 没有键,就让 keys() 也返回值迭代器,这样你在通用代码里不用区分 Map 和 Set,调用 keys() 都不会报错。Symbol.iterator 引用 values(),是 Set 的默认迭代器。

三种写法遍历结果完全相同:

1
2
3
for (let value of s.values()) { /* val1, val2, val3 */ }
for (let value of s[Symbol.iterator]()) { /* val1, val2, val3 */ }
for (let value of s) { /* val1, val2, val3 */ }

第三种最常用——for...of 会自动调用 Symbol.iterator,直接遍历集合实例即可,不需要显式调用任何方法。

扩展操作符:Set 转数组的桥梁

默认迭代器还有一个实用场景:配合扩展操作符,把 Set 转成数组:

1
2
const s = new Set(["val1", "val2", "val3"]);
console.log([...s]); // ["val1", "val2", "val3"]

扩展操作符 ... 内部调用的就是 Symbol.iterator,所以能直接展开 Set。这也引出了数组去重的经典一行代码:

1
2
const unique = [...new Set([1, 2, 2, 3, 3, 3])];
// [1, 2, 3]

先把数组丢进 Set 去重,再展开回数组。简洁、高效、不需要手写循环。

entries():为什么要返回 [value, value]?

Map 的 entries() 返回 [key, value] 二元数组,这很自然。但 Set 没有键,entries() 该返回什么?答案是把值重复一遍,返回 [value, value]

1
2
3
4
5
6
7
8
const s = new Set(["val1", "val2", "val3"]);

for (let pair of s.entries()) {
 console.log(pair);
}
// ["val1", "val1"]
// ["val2", "val2"]
// ["val3", "val3"]

乍一看这个设计很怪——同样的值存两遍有什么意义?理解它的关键在于:Set 的 API 是 Map API 的镜像。当你写一段通用代码,用 entries() 遍历”键值对集合”时,不管传入的是 Map 还是 Set,返回格式统一为二元数组,代码不需要做特殊处理。这是 API 设计上的一致性考虑,不是日常开发中常用的方法。

forEach():回调方式遍历

如果不想用迭代器,forEach() 提供了回调式的遍历方式:

1
2
3
4
5
6
7
8
const s = new Set(["val1", "val2", "val3"]);

s.forEach((val, dupVal) => {
 alert(`${val} -> ${dupVal}`);
});
// val1 -> val1
// val2 -> val2
// val3 -> val3

注意回调的参数:第一个是值,第二个也是值。这个参数顺序和 Map 的 forEach((value, key) => ...) 对应——Map 的第二个参数是键,Set 没有键,就把值填到”键的位置”,保持参数结构一致。

forEach() 还支持第二个参数,用于重写回调内部的 this 指向。但实际开发中箭头函数不受 this 绑定影响,这个参数几乎用不到。

修改值不会影响集合判断:

遍历时修改变量,不会改变集合中的内容。这和 Map 的行为完全一致,原因也相同——要区分两种情况:

原始值:重新赋值只是让变量指向了一个新值,集合中存储的原始值不受影响:

1
2
3
4
5
6
7
const s1 = new Set(["val1"]);

for (let value of s1.values()) {
 value = "newVal"; // 变量重新赋值
 alert(value); // "newVal" — 变量确实变了
 alert(s1.has("val1")); // true — 但集合里的值没变
}

value = "newVal" 只是让循环变量 value 不再指向 "val1",而是指向了 "newVal"。字符串是不可变的原始值,集合中存的 "val1" 原封不动。

对象值:修改对象的属性不会影响 has() 判断,因为 Set 存的是引用,不是内容:

1
2
3
4
5
6
7
8
9
const valObj = {id: 1};
const s2 = new Set([valObj]);

for (let value of s2.values()) {
 value.id = "newVal"; // 修改对象的属性
 alert(s2.has(valObj)); // true — 引用没变,集合还能找到
}

alert(valObj); // {id: "newVal"} — 原对象确实被改了

这两种情况的关键区别在于:原始值赋值是”变量换了一个指向”,对象属性修改是”改了同一个对象的内容”。Set 的 has() 判断依据是引用地址是否相同,对象内容改了没关系,只要引用不变就能找到。

这也意味着一个潜在陷阱:如果你在遍历时修改了集合中对象的属性,别的代码可能读到被修改后的值,因为 Set 和你的变量指向同一个对象。

迭代方法总结:

方法返回内容与 Map 的关系
values()值迭代器Map 也有,行为一致
keys()值迭代器(values() 的别名)Map 返回键迭代器,Set 没有键所以等同于 values()
entries()[value, value]Map 返回 [key, value],Set 把值重复填入键的位置
Symbol.iterator引用 values()Map 引用 entries(),Set 引用 values()
forEach((val, dupVal))回调遍历Map 是 (value, key),Set 第二个参数也是值

贯穿所有方法的一条设计规律:Set 的迭代 API 完全模仿 Map,只不过”键”的位置都用值来填充。这样设计的好处是,写通用代码时不用区分 Map 和 Set,统一按”键值对集合”处理即可。

弱引用:

JavaScript是有垃圾回收的语言,也就是说引擎会自动管理内存中对象的分配和回收。当某个对象不再使用时,垃圾收集器就会释放它占用的内存,以便用于其他目的。

然而,在某些情况下,我们可能希望保持对某个对象的引用,但又不阻止它被当作垃圾回收。这种引用称为弱引用。

弱引用就是不会阻止对象被作为垃圾回收的引用。换句话说,如果指向某个对象的唯一引用是个弱引用,垃圾回收器会释放该对象所占用的内存。

WeakMap 和 WeakSet 比 WeakRef 常见得多。它们是语言内置的弱引用容器,API 简单、语义清晰,很多库都在用。而 WeakRef + FinalizationRegistry 是更底层的原语,灵活但也危险,除非在写基础库,否则基本碰不到。知道是干嘛用的和设计意图就行。

WeakRef:

创建弱引用:

WeakRef 构造函数接收一个参数,即想要弱引用的目标对象:

1
2
let obj = { name: "Matt" };
let weakRef = new WeakRef(obj);

创建之后,弱引用的目标对象就不能再更换了。WeakRef 没有提供修改目标的方法,它在整个生命周期内只指向实例化时传入的那个对象。

deref():访问目标对象

弱引用不会像普通引用那样直接拿到对象,必须通过 deref() 方法访问:

1
2
3
4
let obj = { name: "Matt" };
let weakRef = new WeakRef(obj);

console.log(weakRef.deref()); // { name: "Matt" }

deref() 的返回值有两种情况:

  • 目标对象还活着 → 返回目标对象本身
  • 目标对象已被垃圾回收 → 返回 undefined

这意味着每次调用 deref() 都不知道对象是否还存活:

1
2
3
4
obj = null; // 去掉强引用,对象只剩下弱引用持有了

// 某个时刻,垃圾回收器回收了该对象
console.log(weakRef.deref()); // undefined

obj = null 之后,这个对象只剩 weakRef 还引用着它。但 WeakRef 是弱引用,不会阻止垃圾回收,当引擎认为时机合适时,对象就会被回收,deref() 就会返回 undefined

垃圾回收的时机不可控:

上面的代码看起来很直观:去掉强引用 → 对象被回收 → deref() 返回 undefined。但实际运行时,不会看到这个过程按预期发生。

原因有两个:

  1. 浏览器不会立即回收。垃圾回收是昂贵的操作,浏览器会尽可能推迟执行,等到内存压力大时才触发。obj = null 之后,对象可能还存活很久。
  2. 开发者工具会阻止回收。打开控制台本身就会让浏览器对垃圾回收更加保守,因为控制台需要保持对对象的引用以便你查看。

所以这段代码的注释”垃圾回收之后”是一个逻辑推演,不是能在控制台里稳定复现的结果。WeakRef 的设计初衷不是为了手动控制回收时机,而是用于缓存、监听器注册等场景。在这些场景中,对象存在就用,不存在就重新创建,不需要关心它何时被回收。

FinalizationRegistry:

有时候,我们不仅想”不阻止回收”,还想在对象即将被回收时执行一些清理操作,比如释放关联资源、移除缓存条目。FinalizationRegistry 就是为此而生。

创建注册表与注册对象:

FinalizationRegistry 构造函数接收一个回调函数,这个回调会在注册的对象被垃圾回收之前执行:

1
2
3
let finalizationRegistry = new FinalizationRegistry((heldValue) => {
 console.log(`Cleaning up object: ${heldValue}`);
});

注册对象用 register() 方法,第一个参数是要监控的目标对象,第二个参数是持有值(held value)——这个值会在对象被回收时传入回调函数:

1
2
3
4
5
6
7
let obj = { name: "Matt" };
finalizationRegistry.register(obj, "My held value");

obj = null;

// 垃圾回收之后:
// "Cleaning up object: My held value"

流程很清晰:obj 失去所有强引用 → 垃圾回收器决定回收它 → 回收前触发回调,把 "My held value" 传给 heldValue 参数。

注意,持有值和目标对象是分开的——回调拿到的不是被回收的对象本身(它已经要被销毁了),而是你注册时附带的那个值,通常是一个标识符或键,用来定位需要清理的资源。

注销监控:unregister()

如果改变主意,不再需要监控某个对象,可以用 unregister() 取消注册:

1
2
3
4
5
6
7
8
9
10
11
12
let obj = { name: "Matt" };
let finalizationRegistry = new FinalizationRegistry((heldValue) => {
 console.log(`Cleaning up object: ${heldValue}`);
});

finalizationRegistry.register(obj, "My held value");
obj = null;

finalizationRegistry.unregister(obj);

// 垃圾回收之后:
// <无输出> — 已注销,回调不会触发

unregister() 的参数是注册时的目标对象。注销成功后,即使该对象后来被回收,回调也不会执行。

慎用 FinalizationRegistry:

FinalizationRegistry 是一个需要极其谨慎对待的特性,应该尽可能不用。原因有四:

  1. 垃圾回收行为不可保证。不同引擎(V8、SpiderMonkey、JavaScriptCore)的回收策略不同,同一引擎的不同版本也可能变化。设置的回调在 A 浏览器能触发,在 B 浏览器可能不触发。
  2. 回调执行时机不确定。对象被回收和回调执行之间可能存在很长的延迟:不是”回收瞬间立即执行”,而是”引擎有空时才执行”。
  3. 回调可能很晚才执行。主流实现确实会在程序运行过程中执行清理回调,但可能延迟到对象被回收之后很久。
  4. 回调可能完全不执行。如果程序在回调触发前就终止了(比如页面关闭、Node 进程退出),回调永远没有机会运行。

WeakMap:

“弱映射”(WeakMap)是一种新的集合引用类型,为这门语言带来了增强的键-值对存储机制。WeakMap是Map的“兄弟”类型,其API也是Map的子集。WeakMap中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱映射”中键的方式。

基本API:

可以使用new关键字实例化一个空的WeakMap:

1
const wm = new WeakMap();

弱映射中的键只能是Object或者继承自Object的类型,尝试使用非对象设置键会抛出TypeError。值的类型没有限制。

如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键-值对数组。可迭代对象中的每个键-值都会按照迭代顺序插入新实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const key1 = {id: 1},
      key2 = {id: 2},
      key3 = {id: 3};
// 使用嵌套数组初始化弱映射
const wm1 = new WeakMap([
  [key1, "val1"],
  [key2, "val2"],
  [key3, "val3"]
]);
alert(wm1.get(key1)); // val1
alert(wm1.get(key2)); // val2
alert(wm1.get(key3)); // val3

// 初始化是全有或全无的操作
// 只要有一个键无效就会抛出错误,导致整个初始化失败
const wm2 = new WeakMap([
  [key1, "val1"],
  ["BADKEY", "val2"],
  [key3, "val3"]
]);
// TypeError: Invalid value used as WeakMap key
typeof wm2;
// ReferenceError: wm2 is not defined

// 原始值可以先包装成对象再用作键
const stringKey = new String("key1");
const wm3 = new WeakMap([
  stringKey, "val1"
]);
alert(wm3.get(stringKey)); // "val1"

初始化之后可以使用set()再添加键-值对,可以使用get()和has()查询,还可以使用delete()删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const wm = new WeakMap();

const key1 = {id: 1},
      key2 = {id: 2};

alert(wm.has(key1)); // false
alert(wm.get(key1)); // undefined

wm.set(key1, "Matt")
  .set(key2, "Frisbie");

alert(wm.has(key1)); // true
alert(wm.get(key1)); // Matt

wm.delete(key1);     // 只删除这一个键-值对

alert(wm.has(key1)); // false
alert(wm.has(key2)); // true

set()方法返回弱映射实例,因此可以把多个操作连缀起来,包括初始化声明:

1
2
3
4
5
6
7
8
9
10
11
12
const key1 = {id: 1},
      key2 = {id: 2},
      key3 = {id: 3};

const wm = new WeakMap().set(key1, "val1");

wm.set(key2, "val2")
  .set(key3, "val3");

alert(wm.get(key1)); // val1
alert(wm.get(key2)); // val2
alert(wm.get(key3)); // val3

弱键:

WeakMap中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的只要键存在,键-值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收

来看下面的例子:

1
2
3
const wm = new WeakMap();

wm.set({}, "val");

set()方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键-值对就从弱映射中消失了,使其成为一个空映射。

这种情况obj 变量持有对对象的强引用。即使 WeakMap 的键是弱引用,obj 也在”正式地拿着”这个对象,所以垃圾回收器不会动它,键-值对一直存在。

1
2
3
4
const wm = new WeakMap();
const obj = { name: "Matt" }; // ← obj 变量指向这个对象

wm.set(obj, "val");

在这个例子中,因为值也没有被引用,所以这对键-值被破坏以后,值本身也会成为垃圾回收的目标。

再看一个稍微不同的例子:

1
2
3
4
5
6
7
8
9
10
11
const wm = new WeakMap();

const container = {
  key: {}
};

wm.set(container.key, "val");

function removeReference() {
  container.key = null;
}

这一次,container对象维护着一个对弱映射键的引用(键是 container.key 指向的那个对象 {}),因此这个对象键不会成为垃圾回收的目标。不过,如果调用了removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键-值对清理掉。

不可迭代键:

因为WeakMap中的键-值对任何时候都可能被销毁,所以没必要提供迭代其键-值对的能力。当然,也用不着像clear()这样一次性销毁所有键-值的方法。WeakMap确实没有这个方法。因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱映射中取得值。即便代码可以访问WeakMap实例,也没办法看到其中的内容。

WeakMap实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。

使用弱映射:

因为WeakMap实例不会妨碍垃圾回收,所以非常适合保存关联元数据。来看下面这个例子,其中使用了常规的Map:

1
2
3
4
5
6
const m = new Map();

const loginButton = document.querySelector('#login');

// 给这个节点关联一些元数据
m.set(loginButton, {disabled: true});

假设在上面的代码执行后,页面被JavaScript改变了,原来的登录按钮从DOM树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的DOM节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。

如果这里使用的是弱映射,如以下代码所示,那么当节点从DOM树中被删除后,垃圾回收程序就可以立即释放其内存(假设没有其他地方引用这个对象):

1
2
3
4
5
6
const wm = new WeakMap();

const loginButton = document.querySelector('#login');

// 给这个节点关联一些元数据
wm.set(loginButton, {disabled: true});

WeakSet:

“弱集合”(WeakSet)是一种新的集合引用类型,为这门语言带来了集合数据结构。WeakSet是Set的“兄弟”类型,其API也是Set的子集。WeakSet中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱集合”中值的方式。

基本API:

可以使用new关键字实例化一个空的WeakSet:

1
const ws = new WeakSet();

弱集合中的值只能是Object或者继承自Object的类型,尝试使用非对象设置值会抛出TypeError。

如果想在初始化时填充弱集合,则构造函数可以接收一个可迭代对象,其中需要包含有效的值。可迭代对象中的每个值都会按照迭代顺序插入到新实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const val1 = {id: 1},
      val2 = {id: 2},
      val3 = {id: 3};
// 使用数组初始化弱集合
const ws1 = new WeakSet([val1, val2, val3]);

alert(ws1.has(val1)); // true
alert(ws1.has(val2)); // true
alert(ws1.has(val3)); // true

// 初始化是全有或全无的操作
// 只要有一个值无效就会抛出错误,导致整个初始化失败
const ws2 = new WeakSet([val1, "BADVAL", val3]);
// TypeError: Invalid value used in WeakSet
typeof ws2;
// ReferenceError: ws2 is not defined

// 原始值可以先包装成对象再用作值
const stringVal = new String("val1");
const ws3 = new WeakSet([stringVal]);
alert(ws3.has(stringVal)); // true

初始化之后可以使用add()再添加新值,可以使用has()查询,还可以使用delete()删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const ws = new WeakSet();

const val1 = {id: 1},
      val2 = {id: 2};

alert(ws.has(val1)); // false

ws.add(val1)
  .add(val2);

alert(ws.has(val1)); // true
alert(ws.has(val2)); // true

ws.delete(val1);     // 只删除这一个值

alert(ws.has(val1)); // false
alert(ws.has(val2)); // true

add()方法返回弱集合实例,因此可以把多个操作连缀起来,包括初始化声明:

1
2
3
4
5
6
7
8
9
10
11
12
const val1 = {id: 1},
      val2 = {id: 2},
      val3 = {id: 3};

const ws = new WeakSet().add(val1);

ws.add(val2)
  .add(val3);

alert(ws.has(val1)); // true
alert(ws.has(val2)); // true
alert(ws.has(val3)); // true

弱值:

这部分以及下面的部分和弱映射类似。

WeakSet中“weak”表示弱集合的值是“弱弱地拿着”的。意思就是,这些值不属于正式的引用,不会阻止垃圾回收。

来看下面的例子:

1
2
3
const ws = new WeakSet();

ws.add({});

add()方法初始化了一个新对象,并将它用作一个值。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象值就会被当作垃圾回收。然后,这个值就从弱集合中消失了,使其成为一个空集合。

再看一个稍微不同的例子:

1
2
3
4
5
6
7
8
9
10
11
const ws = new WeakSet();

const container = {
  val: {}
};

ws.add(container.val);

function removeReference() {
  container.val = null;
}

这一次,container对象维护着一个对弱集合值的引用,因此这个对象值不会成为垃圾回收的目标。不过,如果调用了removeReference(),就会摧毁值对象的最后一个引用,垃圾回收程序就可以把这个值清理掉。

不可迭代值:

因为WeakSet中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力。当然,也用不着像clear()这样一次性销毁所有值的方法。WeakSet确实没有这个方法。因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱集合中取得值。即便代码可以访问WeakSet实例,也没办法看到其中的内容。

WeakSet之所以限制只能用对象作为值,是为了保证只有通过值对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。

使用弱集合:

与WeakMap类似,WeakSet适合在不妨碍垃圾回收的情况下跟踪对象。来看下面的例子,这里使用了一个普通Set:

1
2
3
4
5
6
const disabledElements = new Set();

const loginButton = document.querySelector('#login');

// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton);

这样,通过查询元素在不在disabledElements中,就可以知道它是不是被禁用了。不过,假如元素从DOM树中被删除了,它的引用却仍然保存在Set中,因此垃圾回收程序也不能回收它。

为了让垃圾回收程序回收元素的内存,可以在这里使用WeakSet:

1
2
3
4
5
6
const disabledElements = new WeakSet();

const loginButton = document.querySelector('#login');

// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton);

这样,只要WeakSet中任何元素从DOM树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存(假设没有其他地方引用这个对象)。

迭代与扩展操作:

迭代器和扩展操作符对集合引用类型特别有用。这些新特性让集合引用类型之间相互操作、复制和修改变得异常方便。

第7章会更详细地介绍迭代器和生成器。

如本章前面所示,Array、Map、Set和所有定型数组定义了默认迭代器。这意味着上述所有类型都支持顺序迭代,都可以传入for-of循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let iterableThings = [
  Array.of(1, 2),
  typedArr = Int16Array.of(3, 4),
  new Map([[5, 6], [7, 8]]),
  new Set([9, 10])
];

for (const iterableThing of iterableThings) {
  for (const x of iterableThing) {
    console.log(x);
  }
}

// 1
// 2
// 3
// 4
// [5, 6]
// [7, 8]
// 9
// 10

这也意味着所有这些类型都兼容扩展操作符。扩展操作符在对可迭代对象执行浅复制时特别有用,只需简单的语法就可以复制整个对象:

1
2
3
4
5
6
let arr1 = [1, 2, 3];
let arr2 = [...arr1];

console.log(arr1);          // [1, 2, 3]
console.log(arr2);          // [1, 2, 3]
console.log(arr1 === arr2); // false

对于期待可迭代对象的构造函数,只要传入一个可迭代对象就可以实现复制:

1
2
3
4
5
let map1 = new Map([[1, 2], [3, 4]]);
let map2 = new Map(map1);

console.log(map1); // Map {1 => 2, 3 => 4}
console.log(map2); // Map {1 => 2, 3 => 4}

当然,也可以构建数组的部分元素:

1
2
3
4
let arr1 = [1, 2, 3];
let arr2 = [0, ...arr1, 4, 5];

console.log(arr2); // [0, 1, 2, 3, 4, 5]

浅复制意味着只会复制对象引用:

1
2
3
4
5
let arr1 = [{}];
let arr2 = [...arr1];

arr1[0].foo = 'bar';
console.log(arr2[0]); // { foo: 'bar' }

上面的这些类型都支持多种构建方法,比如Array.of()和Array.from()静态方法。在与扩展操作符一起使用时,可以非常方便地实现互操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let arr1 = [1, 2, 3];

// 把数组复制到定型数组
let typedArr1 = Int16Array.of(...arr1);
let typedArr2 = Int16Array.from(arr1);
console.log(typedArr1);   // Int16Array [1, 2, 3]
console.log(typedArr2);   // Int16Array [1, 2, 3]

// 把数组复制到映射
let map = new Map(arr1.map((x) => [x, 'val' + x]));
console.log(map);   // Map {1 => 'val 1', 2 => 'val 2', 3 => 'val 3'}

// 把数组复制到集合
let set = new Set(typedArr2);
console.log(set);   // Set {1, 2, 3}

// 把集合复制回数组
let arr2 = [...set];
console.log(arr2);  // [1, 2, 3]

小结:

JavaScript支持很多高级引用类型,以方便处理数据集合和管理对象引用。对象是键-值对的无序集合,其中键是字符串或符号,而值可以是任意数据类型。对象有很多内置方法,可以操作和遍历对象的属性。

数组是值的有序集合,这些值可以是任意数据类型。数组有很多内置方法,可以添加、删除和操作数组元素。

定型数组是特定类型值的集合,比如Int32Array或Float64Array。这些类型的值适合处理二进制数据以及执行数值操作。

JavaScript支持弱引用对象。弱引用不会阻止垃圾回收器释放被引用的对象,很适合跟踪那些程序不再需要的对象。这些特性对内存管理很重要,在大型复杂程序中有助于防止内存泄漏。

WeakRef提供了创建对象弱引用的途径,而FinalizationRegistry则支持对不再需要的对象执行清理逻辑。WeakSet和WeakMap是只允许对其元素进行弱引用的集合,适合管理不应该妨碍垃圾回收的对象关系。

本文由作者按照 CC BY 4.0 进行授权

重启DAY6 二叉树与递归

阅读DAY8 JavaScript高级程序设计 7章上 迭代器与生成器