A JavaScript Set 2015 óta tudta, hogyan kell egy elemet hozzáadni, törölni és megkeresni. Azt viszont nem tudta, amitől egy halmaz halmaz: uniót, metszetet és különbséget képezni. Erre vagy segédfüggvényt írtunk, vagy egy könyvtárat hívtunk segítségül, mert nyilván egy filter() köré is lehet npm-csomagot építeni.
Az új Set-műveletek ezt rendbe tették. A feltűnő rész mégsem az, hogy végre van intersection(), hanem az, hogy az eredmény sorrendje attól függ, melyik bemeneti halmaz kisebb. Egyetlen, a metszetbe be sem kerülő elem megváltoztathatja a kimenet sorrendjét. Ez nem motorhiba, hanem tudatos specifikációs kompromisszum.

Hét metódus, két fajta eredmény
A TC39 javaslatából hét példánymetódus került az ECMAScriptbe. A union(), intersection(), difference() és symmetricDifference() új Set-et ad vissza; az eredeti halmazokat nem módosítja. Az isSubsetOf(), isSupersetOf() és isDisjointFrom() logikai választ ad.
const enabled = new Set(["search", "export", "billing"]);
const licensed = new Set(["search", "billing"]);
enabled.intersection(licensed); // Set { "search", "billing" }
enabled.difference(licensed); // Set { "export" }
enabled.isSupersetOf(licensed); // true
A metódusok a modern böngészőkben 2024 közepe óta széles körben elérhetők; az MDN az intersection()-t Baseline 2024 funkcióként jelöli. A cikk példáit Node.js 22.22.3 alatt futtattam.
Ugyanaz a metszet, más sorrend
A Set bejárási sorrendje a beszúrási sorrend, ezért kézenfekvő lenne azt várni, hogy a.intersection(b) mindig az a sorrendjét őrzi. Nem ezt teszi:
const wanted = new Set(["read", "write", "delete"]);
const smallPolicy = new Set(["delete", "read"]);
[...wanted.intersection(smallPolicy)];
// ["delete", "read"] — a kisebb, jobb oldali Set sorrendje
const equalPolicy = new Set(["delete", "read", "archive"]);
[...wanted.intersection(equalPolicy)];
// ["read", "delete"] — azonos méretnél a bal oldal sorrendje
Az archive egyik eredményben sincs benne, mégis átbillentette az algoritmust a másik ágra. Ha a metszetet később tömbbé alakítod, renderelési sorrendként használod vagy snapshot tesztbe teszed, ebből remek „ugyan ki rendezte át?” délután lehet. Ha a sorrend az üzleti logika része, rendezd explicit módon; a halmazművelet tagságot garantál, nem prioritást.
A furcsaságért cserébe nem járjuk be az óriáshalmazt
Az ECMAScript algoritmusa előbb összehasonlítja a méreteket. Ha a fogadó Set a kisebb, annak elemein megy végig, és a másik has() metódusával ellenőriz. Ha a paraméter kisebb, annak kulcsait járja be, és a fogadó halmaz belső adataiban keres. Így a munka a kisebb gyűjtemény méretéhez igazodhat.
A sorrend nem véletlenül lett ilyen. A JavaScriptCore megvalósítása miatt az eredeti, stabil sorrendet előíró algoritmus csak a teljes fogadó halmaz bejárásával tudta volna eldönteni két elem relatív helyét. A TC39 erről szóló vitájának vége az lett, hogy elengedték a stabil bal oldali sorrendet. Magyarul: nem fizettetnek ki egy nagy adatszerkezetet csak azért, hogy a kimenet szebb legyen. Szerintem ez helyes döntés, csak a metódus neve ezt udvariasan elhallgatja.
A Map halmazszerű, az Array nem
A jobb oldali argumentumnak nem muszáj valódi Set-nek lennie. Elég a set-like protokoll: legyen numerikus size tulajdonsága, valamint has() és keys() metódusa. A Map pontosan ilyen, ezért a kulcsai halmazként használhatók:
const roles = new Map([
["editor", { label: "Szerkesztő" }],
["admin", { label: "Admin" }],
]);
new Set(["admin", "reader"]).intersection(roles);
// Set { "admin" }
A keys() választása szándékos. A Map alap iterátora kulcs–érték párokat ad, miközben a has() kulcsot vizsgál; a kettő együtt következetlen eredményt adna. A tömb viszont nem set-like: nincs size tulajdonsága, a keys() pedig az indexeit szolgáltatja. Ezért az intersection([2, 3]) TypeError, nem pedig csendes, lassú varázslat. A döntés részletes indoklása szerint az iterálható bemenet automatikus Set-té alakítása lerontaná a kisebb halmazra épített teljesítményígéretet. Ha tömböd van, írd ki a költséget: set.intersection(new Set(array)).
Az objektum továbbra sem adatbázisrekord
A kényelmesebb API nem változtatja meg az azonosság szabályait. A Set SameValueZero összehasonlítást használ: a NaN azonos a NaN-nal, a -0 és 0 ugyanaz az elem, az objektumok viszont csak azonos referenciával egyeznek. Két külön létrehozott { id: 1 } objektum metszete üres. Ha rekordokat akarsz azonosítani, előbb stabil primitív kulcsokra — például ID-kra — képezd le őket.
Még egy apró csavar: az új műveletek mindig sima Set-et hoznak létre, akkor is, ha a fogadó objektum egy Set-alosztály. Nem használják a sok meglepetést okozó Symbol.species mechanizmust, és egy felülírt Set.prototype.add sem kap szerepet az eredmény felépítésében. Az alosztálynak saját metódust kell adnia, ha a típusát meg akarja őrizni.
Használnám productionben?
Igen, ha a támogatott futtatókörnyezetekben elérhető, vagy van specifikációhű polyfill. Jogosultságok, feature flag-ek, kijelölt azonosítók és cache-kulcsok metszeténél olvashatóbb, mint az ezredik [...a].filter(x => b.has(x)). Nem használnám automatikusan olyan feldolgozási láncban, ahol a sorrend jelentéssel bír, és objektumrekordok deduplikálását sem bíznám rá.
A kompromisszum tiszta: tömör, natív halmazműveletet kapunk, amely a kisebb bemenethez igazítja a munkát, cserébe az intersection() sorrendje nem a bal oldal szerződése. Aki ebből listát csinál, annak a rendezés továbbra is saját feladata. A JavaScript ezúttal legalább nem tesz úgy, mintha az ingyen lenne.