请解释 Set、Map、WeakSet 和 WeakMap 的区别?

2024年4月8日

💎 加入 E+ 成長計畫 與超過 300+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

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)。

  • 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 执行环境会在特定的时间点执行垃圾回收。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們