请解释 Set、Map、WeakSet 和 WeakMap 的区别?
2024年4月8日
Set
Set 这个数据结构类似数组,但是里面的元素值都是唯一,不会有重复的值,无论此值是原始型别 (primitive values) 或引用型别 (object references)。在 JavaScript 当中,Set 本身是一种构造函式,用来生成 Set 这种数据结构,具体的做法是透过 new Set()
来生成实例。
Set 常见操作方法有
add(value)
:新增值至 Set 中delete(value)
:删除 Set 中的特定值has(value)
:检查 Set 中是否存在特定值size
:获取 Set 中元素的数量
Set 中没有键值(Key),因此使用 entries() 遍历时,返回的元素将是 [value, value] 的形式:
const set1 = new Set();
set1.add(42);
set1.add("forty two");
const iterator1 = set1.entries();
for (const entry of iterator1) {
console.log(entry);
// 预期输出: [42, 42]
// 预期输出: ["forty two", "forty two"]
}
Set 和 WeakSet 的区别
WeakSet 的方法和使用部分与 Set 资料结构相近,本区块会专注在这两者不同之处
WeakSet 内的元素值只允许是物件(Object),但 Set 可接受各种资料类型的值
const wSet = new WeakSet(); const a = [1, 2, 3]; const b = { name: "explainthis" }; wSet.add(a); // WeakSet {Array(3)} wSet.add(b); // WeakSet {{...}} wSet.add(1); // Uncaught TypeError: Invalid value used in weak set
WeakSet 内的元素都是 “弱引用”(weak reference),可以被垃圾回收机制回收。假如使用 Set,即使某个被存入的值,在其他地方已经没有被引用,该值仍会存在于 Set 当中,不会被垃圾回收。但如果是 WeakSet,则会被垃圾回收。如果要更有意识地做记忆体管理,WeakSet 在许多时候能派上用场。
const disableElements = new WeakSet(); const loginButton = document.querySelector("#login"); disableElements.add(loginButton); disableElements.has(loginButton); // true
Map
类似于 Object 的资料结构,都是用键与值 (key-value pair) 的形式储存资料格式,但还是有许多差异,详细可以参考这篇 《在 JavaScript 中,Map 与 object 的差别?为什么有 object 还需要 Map?》一文。Map 本身是一种构造函式,用来生成 Map 这种数据结构,具体做法是 new Map()
来生成实例。
Map 常见操作方法包括:
set(key, value)
:新增元素至 Map 中get(key)
:通过键 (Key) 查询特定元素并返回has(key)
:检查 Map 中是否存在特定键 (Key)delete(key)
:透从 Map 中删除特定元素size
:获取 Map 中元素的数量
Map 常见遍历方法 (遍历顺序与元素放入 Map 的顺序相同):
values()
:返回 Map 中所有元素的值keys()
:返回 Map 中所有元素的键entries()
:返回 Map 中所有的元素,返回的会是[key, value]
的形式
Map 和 WeakMap 的区别
WeakMap 的方法和使用部分与 Map 资料结构相近,但有以下区别:
WeakMap 中的键名 (Key) 只能是物件 (Object) 和 Symbol,不接受其他资料类型作为键名,例如原始值 (primitive values) 如字串、数字、布林值等,但不包括 null。相比之下,Map 可以接受各种资料类型作为键名 (Key)。
- 备注:Symbol 作为键名 (key) 为 ES2023 新增特性(详情可以参考此文章《ES2023(ES14)有什么新特性?》)
WeakMap 中的键名是“弱引用”(weak reference),键名 (key) 所指向的对象可以被垃圾回收,此时的键名 (key) 是无效的
// 如果放入的物件在外面没有其他引用,在 WeakMap 中会被垃圾回收掉
let food = new WeakMap();
let fruit = { name: "apple" };
food.set(fruit, "good"); // 将 fruit 物件置入 WeakMap 中
fruit = null; // 移除 fruit 的引用
console.log(food);
// 因为 JavaScript 的垃圾回收时机会因为不同引擎而有差异,所以可能不会马上被回收,以上可能 log 出两种情境
// WeakMap {Object => "good"},fruit 的引用被移除,但物件可能还未被垃圾回收
// WeakMap(0) fruit 已经被垃圾回收,因此 WeakMap 中没有项目
// 一般的 Map,即使放入的物件在外面没有其他引用,仍在 Map 当中存放
let food = new Map();
let fruit = { name: "apple" };
food.set(fruit, "good");
console.log(food); // Map(1)
fruit = null;
console.log(food); // Map(1) fruit 不会被垃圾回收
在上方的强引用代码中,虽然 fruit
物件最后被重新赋值为 null
(意思等同于无法再透过 fruit
变数获取该对象值,因为其中的引用被断开),但由于 food 与此物件间存在强引用,所以被保留在记忆体中,这就是前面提到的,强引用会防止物件被垃圾回收,并将物件保留在记忆体当中; 弱引用则相反,并不能防止物件被垃圾回收,当 JavaScript 执行环境执行垃圾回收时,上述弱引用例子中的 fruit
物件会被从记忆体和 WeakMap 中删除。
弱引用的适用情境在于,如果引用的物件在未来可能会被删除的情况、且不想防止被垃圾回收时,就适合用 WeakMap 或 WeakSet。例如,如果我们想要记录一些与 DOM 节点相关的数据,有一种方法是使用 Expando 扩充节点上的资讯,但坏处是会直接修改到这个 DOM 节点、且如果未来这个节点被移除时,相关资讯不会被垃圾回收掉,这时如果是使用 WeakMap 就会是很好的替代方案。
备注:如果直接将弱引用代码的例子在 JavaScript 执行环境中执行,可能还是会看到 WeakMap 中有值,这是因为 JavaScript 执行环境会在特定的时间点执行垃圾回收。