JavaScript Set 自 ES2015 規範引入以來,一直被視為功能不全。但現在,這一狀況即將得到改變。
Sets 是一種集合類型,能夠確保其中的值唯一不重複。在 ES2015 版本中,Set 提供的功能主要限於創建、添加、刪除元素及檢查元素是否屬於某個 Set。如果需要對多個集合進行操作或比較,則需要自行編寫函數來實現。幸運的是,ECMAScript 規範的制定委員會 TC39 以及各大瀏覽器開發商已經在這方面取得了進展。目前,我們已經能在 JavaScript 中使用 union(並集)、intersection(交集)和 difference(差集)等操作了。
在深入了解這些新功能之前,讓我們先回顧一下現有的 JavaScript Sets 能做什麼,然後再探討下面的新 Set 函數以及支持這些功能的 JavaScript 引擎。
ES2015 版本的 JavaScript Sets 能完成哪些操作?#
通過一些實例來探討 JavaScript Set 的基本功能是最直接的方法。
你可以創建一個空的 Set,或者通過提供一個可迭代對象(如數組)來初始化一個 Set。
const languages = new Set(["JavaScript", "TypeScript", "HTML", "JavaScript"]);
由於 Set 中的值必須唯一,上述 Set 實際上包含三個元素。可以通過 Set 的 size 屬性來確認這一點。
languages.size;
// => 3
使用 add 方法可以向 Set 中添加新元素。如果嘗試添加的元素已存在,則不會有任何變化。
languages.add("JavaScript");
languages.add("CSS");
languages.size;
// => 4
可以通過 delete 方法從 Set 中移除元素。
languages.delete("TypeScript");
languages.size;
// => 3
使用 has 方法可以檢查某個元素是否屬於 Set。與數組相比,Set 在這方面的檢查效率更高,因為這一操作的時間複雜度為常數時間(O(1))。
languages.has("JavaScript");
// => true
languages.has("TypeScript");
// => false
你還可以使用 forEach 或 for...of 循環遍歷 Set 的元素。元素的排列順序是按照它們被添加到 Set 中的順序。
languages.forEach(element => console.log(element));
// "JavaScript"
// "HTML"
// "CSS"
此外,你可以通過 keys、values(實際上和 keys 等價)及 entries 方法從 Set 獲取迭代器。
最後,可以使用 clear 方法來清空一個 Set。
languages.clear();
languages.size;
// => 0
這是對使用 ES2015 規範的 Set 可執行操作的簡單回顧:
Set提供了處理唯一值集合的方法。- 向
Set添加元素以及測試元素是否存在於Set中都非常高效。 - 將
Array或其他可迭代對象轉換為Set是一種簡便的去重方法。
然而,這種實現缺少了對 Set 之間進行操作的能力。你可能希望合併兩個 Set 以創建一個包含兩者所有元素的新 Set(並集)、找出兩個 Set 的共同元素(交集),或者確定一個 Set 有而另一個 Set 沒有的元素(差集)。直到最近,實現這些操作都需要自定義函數。
新的 Set 函數包括哪些?#
Set 方法的提案為 Set 實例添加了以下方法:union(並集)、intersection(交集)、difference(差集)、symmetricDifference(對稱差集)、isSubsetOf(子集判斷)、isSupersetOf(超集判斷)和 isDisjointFrom(判斷是否不相交)。
這些方法中的一些與 SQL 中的某些連接操作相似,我們將通過代碼示例來展示每個函數的作用。
以下代碼示例可在 Chrome 122+ 或 Safari 17+ 中嘗試。
Set.prototype.union(other)#
兩個集合的並集是一個包含兩個集合中所有元素的集合。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const allLanguages = frontEndLanguages.union(backEndLanguages);
// => Set {"JavaScript", "HTML", "CSS", "Python", "Java"}
在這個例子中,第一個和第二個集合中的所有語言都出現在第三個集合中。與其他向 Set 添加元素的方法一樣,重複的元素會被自動去除。
這相當於對兩個表執行 SQL 的 FULL OUTER JOIN。

Set.prototype.intersection(other)#
兩個集合的交集是一個包含兩個集合中共有元素的集合。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const frontAndBackEnd = frontEndLanguages.intersection(backEndLanguages);
// => Set {"JavaScript"}
這裡,“JavaScript” 是唯一同時出現在兩個集合中的元素。
交集相當於 SQL 中的 INNER JOIN。

Set.prototype.difference(other)#
操作的集合與另一個集合之間的差集包含了第一個集合獨有的所有元素。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const onlyFrontEnd = frontEndLanguages.difference(backEndLanguages);
// => Set {"HTML", "CSS"}
const onlyBackEnd = backEndLanguages.difference(frontEndLanguages);
// => Set {"Python", "Java"}
在確定兩個集合之間的差異時,調用差集函數的集合和作為參數的集合的順序非常重要。在上述例子中,從前端語言集合中移除後端語言集合的結果是 “JavaScript” 被去除,留下了 “HTML” 和 “CSS”。反之,從後端語言集合中移除前端語言集合仍然會去除 “JavaScript”,並留下 “Python” 和 “Java”。
差集類似於執行 SQL 中的 LEFT JOIN。

Set.prototype.symmetricDifference(other)#
兩個集合的對稱差集是一個包含了兩個集合中獨有的所有元素的集合。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const onlyFrontEnd = frontEndLanguages.symmetricDifference(backEndLanguages);
// => Set {"HTML", "CSS", "Python", "Java"}
const onlyBackEnd = backEndLanguages.symmetricDifference(frontEndLanguages);
// => Set {"Python", "Java", "HTML", "CSS"}
在這種情況下,儘管結果集中的元素相同,但元素的順序因所調用的集合不同而有所不同。元素的添加順序由它們被添加到集合中的順序決定,而對函數進行操作的集合的元素會首先被添加。
對稱差集類似於 SQL 中排除兩個表共有元素的 FULL OUTER JOIN。

Set.prototype.isSubsetOf(other)#
如果第一個集合中的所有元素都出現在第二個集合中,則該集合是另一個集合的子集。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const declarativeLanguages = new Set(["HTML", "CSS"]);
declarativeLanguages.isSubsetOf(frontEndLanguages);
// => true
frontEndLanguages.isSubsetOf(declarativeLanguages);
// => false
任何集合都是其自身的子集。
frontEndLanguages.isSubsetOf(frontEndLanguages);
// => true
Set.prototype.isSupersetOf(other)#
如果第一個集合包含第二個集合中的所有元素,則該集合是另一個集合的超集。這是成為子集的相反關係。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const declarativeLanguages = new Set(["HTML", "CSS"]);
declarativeLanguages.isSupersetOf(frontEndLanguages);
// => false
frontEndLanguages.isSupersetOf(declarativeLanguages);
// => true
任何集合都是其自身的超集。
frontEndLanguages.isSupersetOf(frontEndLanguages);
// => true
Set.prototype.isDisjointFrom(other)#
如果兩個集合沒有共同的元素,則它們是不相交的。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const interpretedLanguages = new Set(["JavaScript", "Ruby", "Python"]);
const compiledLanguages = new Set(["Java", "C++", "TypeScript"]);
interpretedLanguages.isDisjointFrom(compiledLanguages);
// => true
frontEndLanguages.isDisjointFrom(interpretedLanguages);
// => false
在這些例子中,解釋型語言和編譯型語言的集合沒有交集,因此這些集合是不相交的。而前端語言和解釋型語言的集合由於共有 “JavaScript” 這一元素,因此它們不是不相交的。
支持情況#
截至本文撰寫之時,這些新增的 Set 方法提案在 TC39 的標準制定流程中已進入第 3 階段,Safari 17(2023 年 9 月發布)和 Chrome 122(2024 年 2 月)已經實現了這些方法。Edge 緊隨 Chrome 之後發布,而 Firefox Nightly 已在實驗性標誌後支持這些功能,預計這兩個瀏覽器很快也會正式支持。
Bun 同樣採用了 Safari 的 JavaScriptCore 引擎,因此已經支持這些新功能。Chrome 對這些功能的支持意味著它們已經被集成到了 V8 JavaScript 引擎中,並且不久將被 Node.js 采納。
希望這意味著這些提案將順利過渡到流程的第 4 階段,並可能及時成為 ES2024 規範的一部分。
Polyfills#
如果你需要在舊版 JavaScript 引擎上獲得這些功能的支持,可以使用 polyfills。這些 polyfills 可以通過 core-js 獲得,或作為 es-shims 項目中的單獨包提供(例如,用於實現並集功能的 set.prototype.union 包)。
如果你已經為這些功能編寫了自己的實現,建議首先遷移到這些 polyfills,隨著這些功能的廣泛支持,逐步淘汰自定義實現。
Sets 的功能不再感覺缺失#
JavaScript Set 長期以來被視為功能不完整,但這七個新函數的加入使其功能更加全面。將這類功能內建於語言本身,意味著我們可以減少對外部依賴或自行實現的需求,更專注於解決實際問題。
這只是 TC39 當前審議的眾多第 3 階段提案中的一部分。查看這份列表,了解 JavaScript 接下來可能加入的新功能。我特別關注 Temporal 和 Decorators,這兩項提案可能會改變我們編寫 JavaScript 的重要部分的方式。