「Set」物件
本文將介紹「Set」物件。
我們將透過實際範例說明「Set」物件。
YouTube Video
「Set」物件
「Set」是一種能用來處理唯一且不重複數值集合的內建物件。它比起陣列更簡單地進行去重、檢查是否存在,並讓像是聯集、交集等集合運算更容易實作。
基礎:建立與使用 Set
首先,讓我們看看如何建立「Set」、新增與移除元素、檢查元素是否存在與取得長度。
以下是建立新的「Set」並展示「add」、「has」、「delete」與「size」的基本範本。
1// Create a Set and demonstrate add, has, delete, and size
2const s = new Set();
3
4s.add(1);
5s.add(2);
6s.add(2); // duplicate, ignored
7
8console.log(s.has(1)); // true
9console.log(s.has(3)); // false
10
11s.delete(2);
12console.log(s.size); // 1
13
14console.log([...s]); // [1]
- 如同這段程式碼所示,
Set會自動移除重複的基本值,並且你可以用size來取得元素的數量。
遍歷方法
Set 是可迭代的,因此你可以用 for...of 或 forEach 來對其進行迭代。順序即為插入的順序。
以下是「for...of」與「forEach」的典型用法。
1// Iterate a Set with for...of and forEach
2const s = new Set(['a', 'b', 'c']);
3
4for (const v of s) {
5 console.log('for...of:', v);
6}
7
8s.forEach((value, sameValue, setRef) => {
9 // Note: second arg is same as first for Set API to match Map signature
10 console.log('forEach:', value);
11});- 「forEach」的回呼函式簽名為「value, value, set」(與 Map 相容),但實際上通常只需用到第一個「value」參數。
陣列與 Set 的互轉(去重實用技巧)
這裡展示了陣列去重的簡單技巧,以及如何將「Set」轉回陣列。
以下範例說明如何通過「Set」將陣列去重。
1// Deduplicate an array using Set
2const arr = [1, 2, 2, 3, 3, 3];
3const deduped = [...new Set(arr)];
4console.log(deduped); // [1, 2, 3]
5
6// Convert a Set to an array using Array.from
7const s = new Set([4, 5, 6]);
8const arrFromSet = Array.from(s);
9console.log(arrFromSet); // [4, 5, 6]
- 這種寫法簡短又快速,非常常用於陣列去重。對基本數值類型特別有效。
物件與參照的處理
「Set」內的物件依照參照來做比較,因此即使內容相同,不同實例也會被視為不同元素。
下方範例展示將物件加入「Set」時的行為。
1// Objects are compared by reference in a Set
2const obj1 = { x: 1 };
3const obj2 = { x: 1 };
4
5const s = new Set();
6s.add(obj1);
7s.add(obj2);
8
9console.log(s.size); // 2 (different references)
10console.log(s.has(obj1)); // true
11console.log(s.has({ x: 1 })); // false (different object)
- 物件的去重判斷依據是參照身分;若要依據物件內容去重需要先序列化或另外處理。
特殊數值:「NaN」與「-0/+0」的處理
「Set」使用 Same-value-zero 比較規則來判斷數值相等性。這種比較方式針對數值有下列特點:。
- 「NaN」被視為與「NaN」相等。
- 正的「+0」與負的「-0」不作區分,視為相同值。
因此,當你將這些值加入「Set」時會出現以下行為:。
1// NaN and zero behavior in Set
2const s = new Set();
3
4s.add(NaN);
5s.add(NaN);
6console.log(s.size); // 1 (NaN considered the same)
7
8s.add(+0);
9s.add(-0);
10console.log(s.size); // still 2 (NaN + 0)
11console.log([...s]); // [NaN, 0] (order may vary but only one zero)
- 一般比較(「NaN === NaN」)會是「false」,但在「Set」裡所有「NaN」都會被視為 '同一個值'。
- +0 和 -0 在數學上可以區分,但在「Set」中都被當作「0」處理。
- 因此,「Set」裡只會剩下一個「NaN」與一個「0」。
- 「Set」的比較規則與「Object.is」類似,但並不完全相同。「Object.is(+0, -0)」會回傳「false」,但在「Set」裡這兩者視作相同。請注意這個差異。
常用技巧:集合運算(聯集、交集、差集)
利用「Set」能更直觀表達集合運算。以下是常見實作範例。
這裡是「union」(聯集)、「intersection」(交集)和「difference」(差集)等函式的實作範例。
1// Set operations: union, intersection, difference
2function union(a, b) {
3 return new Set([...a, ...b]);
4}
5
6function intersection(a, b) {
7 return new Set([...a].filter(x => b.has(x)));
8}
9
10function difference(a, b) {
11 return new Set([...a].filter(x => !b.has(x)));
12}
13
14// Demo
15const A = new Set([1, 2, 3]);
16const B = new Set([3, 4, 5]);
17
18console.log('union', [...union(A, B)]); // [1,2,3,4,5]
19console.log('intersection', [...intersection(A, B)]); // [3]
20console.log('difference A\\B', [...difference(A, B)]); // [1,2]
- 集合運算可以簡單地用「Set」搭配陣列以及篩選(filter)完成。處理大量數據時,「has」的 O(1) 執行效率可大幅提升運算速度。
實戰範例:找出陣列差異(檢查新增/移除項目)
下列範例說明如何運用「Set」來找出兩個陣列(舊清單與新清單)的差異。這樣就能分別檢出哪些項目是新增、哪些是被移除的。
1// Find added and removed items between two arrays
2function diffArrays(oldArr, newArr) {
3 const oldSet = new Set(oldArr);
4 const newSet = new Set(newArr);
5
6 const added = [...newSet].filter(x => !oldSet.has(x));
7 const removed = [...oldSet].filter(x => !newSet.has(x));
8
9 return { added, removed };
10}
11
12const oldList = [1, 2, 3];
13const newList = [2, 3, 4, 5];
14
15console.log(diffArrays(oldList, newList));
16// { added: [4,5], removed: [1] }
- 這個方法在判斷 ID 清單、標籤清單等內容變動時非常方便。用於基本型別時最簡單。
WeakSet 與 Set 的差異(記憶體管理)
WeakSet 和 Set 類似,但它使用弱引用,因此其中的項目可以被垃圾收集機制回收。以下展示「WeakSet」的基本用法。
1// WeakSet basics (objects only, not iterable)
2const ws = new WeakSet();
3let obj = { id: 1 };
4ws.add(obj);
5
6console.log(ws.has(obj)); // true
7
8obj = null; // Now the object is eligible for GC; WeakSet won't prevent collection
「WeakSet」只能保存物件,且無法被遍歷。下方舉例「WeakSet」的限制——僅能保存物件且不能遍歷。
1// WeakSet basics (objects only, not iterable)
2const ws = new WeakSet();
3
4// --- Only objects can be added ---
5try {
6 ws.add(1); // number
7} catch (e) {
8 console.log("Error: WeakSet can only store objects. Adding a number is not allowed.");
9}
10
11try {
12 ws.add("text"); // string
13} catch (e) {
14 console.log("Error: WeakSet can only store objects. Adding a string is not allowed.");
15}
16
17// --- WeakSet is not iterable ---
18try {
19 for (const value of ws) {
20 console.log(value);
21 }
22} catch (e) {
23 console.log("Error: WeakSet is not iterable. You cannot use for...of to loop over its elements.");
24}
25
26// --- Cannot convert to array ---
27try {
28 console.log([...ws]);
29} catch (e) {
30 console.log("Error: WeakSet cannot be converted to an array because it does not support iteration.");
31}
32
33// The object becomes eligible for garbage collection
34let obj = { id: 1 };
35ws.add(obj);
36obj = null;- 「WeakSet」適合臨時標記物件是否存在,但無法遍歷也無法取得長度。
效能與使用時機選擇
決定是否使用「Set」時,需了解其效能特色與資料本身的特性。
- 「has」、「add」、「delete」等操作平均能維持近乎 O(1) 效能。因此在需要大量檢查是否存在或去重的場景,「Set」往往比陣列更有優勢。
- 若要依內容(value)對物件去重則需注意。因為「Set」比較的是參照,若要以內容去重,實務上可以用ID 或其他鍵值,或者將物件序列化成基本型別再丟進 Set。
- 對於小 ~ 中型集合,「Set」特別有助於提升程式碼可讀性。反之,若你要處理極大量元素或頻繁進行陣列/Set 互轉,建議實際進行性能測試。
常見陷阱
「Set」很方便,但若未了解其規格,可能遇到意料外的行為而產生困惑。這裡列出幾個需要注意的典型重點:。
- 物件是以參照來比較,內容相同但不同實例不會被視為重複。
- 「Set」保留插入順序,但無法像陣列那樣以索引存取元素。如需用索引存取,請先將「Set」轉成陣列。
- 「WeakSet」不可遍歷,且僅能存放物件。請注意它的應用較為有限。
- 「NaN」都視為同一值,「+0」及「-0」不區分。這是由於 Same-value-zero 的比較規則。
總結
「Set」是一個直觀且方便處理唯一值集合的資料結構。可以用來為陣列去重、快速查找是否存在,或是以簡潔易讀的程式碼實作聯集、交集等集合運算。
但由於物件以參照比較,若需依內容判斷相等就需額外處理。
認識這些特性並妥善運用,「Set」就能成為提升程式碼可讀性與維護性的有力工具。
您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。