javascript - Enklaste sättet att slå ihop ES6 Maps/Sets?
Finns det ett enkelt sätt att slå ihop ES6 Maps (som Object.assign)? Och medan vi ändå är igång, hur är det med ES6 Sets (som Array.concat)?
Svar #1
För mängder:
var merged = new Set([...set1, ...set2, ...set3])
För kartor:
var merged = new Map([...map1, ...map2, ...map3])
Observera att om flera kartor har samma nyckel, kommer värdet i den sammanslagna kartan att vara värdet från den sista sammanslagningskartan med den nyckeln.
Svar #2
Av skäl jag inte förstår kan du inte direkt lägga till innehållet i en Set till en annan med en inbyggd metod. Operationer som union, intersect, merge, etc... är ganska grundläggande set-operationer, men är inte inbyggda. Lyckligtvis kan du konstruera dessa själv ganska enkelt (se koden nedan).
[Tillagt 2021] - Det finns nu ett förslag om att lägga till nya Set/Map-metoder för dessa typer av operationer, men tidpunkten för implementering är inte omedelbart klar. De verkar vara i steg 2 (utkaststadiet) av specifikationsprocessen.
[Tillagd 2023] - Förslaget är nu i Steg 3 (Kandidatsstadiet) av specifikationsprocessen.
[Tillagt i juni 2024] - Det finns implementationer av detta förslag som levereras nu i Chrome 122, Edge (osäker på vilken version), Firefox 127, Safari 17.0 och Nodejs 23.0.0.
För att själv implementera en sammanslagningsoperation (slå samman innehållet i en Set till en annan eller en Map till en annan), kan du göra detta med en enda .forEach()-rad:
var s = new Set([1,2,3]);
var t = new Set([4,5,6]);
t.forEach(s.add, s);
console.log(s); // 1,2,3,4,5,6
Och för en Map kan du göra så här:
var s = new Map([["key1", 1], ["key2", 2]]);
var t = new Map([["key3", 3], ["key4", 4]]);
t.forEach(function(value, key) {
s.set(key, value);
});
Eller, i ES6-syntax:
t.forEach((value, key) => s.set(key, value));
[Tillagt 2021]
Eftersom det nu finns ett officiellt förslag för nya Set-metoder, kan du använda denna polyfill för den föreslagna .union()-metoden som skulle fungera i ES6+ versioner av ECMAScript. Observera, enligt specifikationen, att detta returnerar en ny Set som är unionen av två andra set. Det slår inte samman innehållet i ett set med ett annat och detta implementerar typkontrollen som anges i förslaget.
if (!Set.prototype.union) {
Set.prototype.union = function(iterable) {
if (typeof this !== "object") {
throw new TypeError("Must be of object type");
}
const Species = this.constructor[Symbol.species];
const newSet = new Species(this);
if (typeof newSet.add !== "function") {
throw new TypeError("add method on new set species is not callable");
}
for (item of iterable) {
newSet.add(item);
}
return newSet;
}
}
Eller, här är en mer komplett version som modellerar ECMAScript-processen för att få artens konstruktor mer fullständigt och har anpassats för att köras på äldre versioner av Javascript som kanske inte ens har Symbol eller har Symbol.species satt:
if (!Set.prototype.union) {
Set.prototype.union = function(iterable) {
if (typeof this !== "object") {
throw new TypeError("Must be of object type");
}
const Species = getSpeciesConstructor(this, Set);
const newSet = new Species(this);
if (typeof newSet.add !== "function") {
throw new TypeError("add method on new set species is not callable");
}
for (item of iterable) {
newSet.add(item);
}
return newSet;
}
}
function isConstructor(C) {
return typeof C === "function" && typeof C.prototype === "object";
}
function getSpeciesConstructor(obj, defaultConstructor) {
const C = obj.constructor;
if (!C) return defaultConstructor;
if (typeof C !== "function") {
throw new TypeError("constructor is not a function");
}
// use try/catch here to handle backward compatibility when Symbol does not exist
let S;
try {
S = C[Symbol.species];
if (!S) {
// no S, so use C
S = C;
}
} catch (e) {
// No Symbol so use C
S = C;
}
if (!isConstructor(S)) {
throw new TypeError("constructor function is not a constructor");
}
return S;
}
Till din kännedom, om du vill ha en enkel subklass av det inbyggda Set-objektet som innehåller en .merge()-metod, kan du använda detta:
// subclass of Set that adds new methods
// Except where otherwise noted, arguments to methods
// can be a Set, anything derived from it or an Array
// Any method that returns a new Set returns whatever class the this object is
// allowing SetEx to be subclassed and these methods will return that subclass
// For this to work properly, subclasses must not change behavior of SetEx methods
//
// Note that if the contructor for SetEx is passed one or more iterables,
// it will iterate them and add the individual elements of those iterables to the Set
// If you want a Set itself added to the Set, then use the .add() method
// which remains unchanged from the original Set object. This way you have
// a choice about how you want to add things and can do it either way.
class SetEx extends Set {
// create a new SetEx populated with the contents of one or more iterables
constructor(...iterables) {
super();
this.merge(...iterables);
}
// merge the items from one or more iterables into this set
merge(...iterables) {
for (let iterable of iterables) {
for (let item of iterable) {
this.add(item);
}
}
return this;
}
// return new SetEx object that is union of all sets passed in with the current set
union(...sets) {
let newSet = new this.constructor(...sets);
newSet.merge(this);
return newSet;
}
// return a new SetEx that contains the items that are in both sets
intersect(target) {
let newSet = new this.constructor();
for (let item of this) {
if (target.has(item)) {
newSet.add(item);
}
}
return newSet;
}
// return a new SetEx that contains the items that are in this set, but not in target
// target must be a Set (or something that supports .has(item) such as a Map)
diff(target) {
let newSet = new this.constructor();
for (let item of this) {
if (!target.has(item)) {
newSet.add(item);
}
}
return newSet;
}
// target can be either a Set or an Array
// return boolean which indicates if target set contains exactly same elements as this
// target elements are iterated and checked for this.has(item)
sameItems(target) {
let tsize;
if ("size" in target) {
tsize = target.size;
} else if ("length" in target) {
tsize = target.length;
} else {
throw new TypeError("target must be an iterable like a Set with .size or .length");
}
if (tsize !== this.size) {
return false;
}
for (let item of target) {
if (!this.has(item)) {
return false;
}
}
return true;
}
}
module.exports = SetEx;
Detta är tänkt att vara i sin egen fil setex.js som du sedan kan require() in i node.js och använda istället för den inbyggda Set.
Svar #3
Här är min lösning med generatorer:
För kartor:
let map1 = new Map(), map2 = new Map();
map1.set('a', 'foo');
map1.set('b', 'bar');
map2.set('b', 'baz');
map2.set('c', 'bazz');
let map3 = new Map(function*() { yield* map1; yield* map2; }());
console.log(Array.from(map3)); // Result: [ [ 'a', 'foo' ], [ 'b', 'baz' ], [ 'c', 'bazz' ] ]
För set:
let set1 = new Set(['foo', 'bar']), set2 = new Set(['bar', 'baz']);
let set3 = new Set(function*() { yield* set1; yield* set2; }());
console.log(Array.from(set3)); // Result: [ 'foo', 'bar', 'baz' ]
Svar #4
Chrome 122 och Safari 17 levererar nya Set-metoder (MDN-dokumentation, caniuse-tabell):
const union = setA.union(setB);
Vilket är bra, men det skapar en ny uppsättning varje gång.
Om du vill ändra ett befintligt objekt istället, använd en hjälpfunktion:
Set
function setUnion(set, ...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
set.add(item);
}
}
}
Användning :
const setA = new Set([1, 2, 3]);
const setB = new Set([4, 5, 6]);
const setC = new Set([7, 8, 9]);
setUnion(setA, setB, setC);
// setA will have items 1, 2, 3, 4, 5, 6, 7, 8, 9
Karta
function mapUnion(map, ...iterables) {
for (const iterable of iterables) {
for (const item of iterable) {
map.set(...item);
}
}
}
Användning :
const mapA = new Map().set('S', 1).set('P', 2);
const mapB = new Map().set('Q', 3).set('R', 4);
mapUnion(mapA, mapB);
// mapA will have items ['S', 1], ['P', 2], ['Q', 3], ['R', 4]
Svar #5
Redigera:
Jag jämförde min ursprungliga lösning med andra lösningar som föreslås här och upptäckte att den är mycket ineffektiv.
Själva referensvärdet är mycket intressant (länk) Det jämför 3 lösningar (högre är bättre):
- @fregante (tidigare kallad @bfred.it) lösning, som lägger till värden ett efter ett (14 955 op/s)
- @jameslk:s lösning, som använder en själv-anropande generator (5 089 op/s)
- min egen, som använder reduce & spread (3 434 op/sek)
Som du kan se är @fregantes lösning definitivt vinnaren.
Prestanda + Oföränderlighet
Med det i åtanke, här är en något modifierad version som inte ändrar den ursprungliga mängden och accepterar ett variabelt antal itererbara objekt att kombinera som argument:
function union(...iterables) { const set = new Set(); for (const iterable of iterables) { for (const item of iterable) { set.add(item); } } return set; }Användning:
const a = new Set([1, 2, 3]); const b = new Set([1, 3, 5]); const c = new Set([4, 5, 6]); union(a,b,c) // {1, 2, 3, 4, 5, 6}
Originalt svar
Jag skulle vilja föreslå en annan metod, med reduce och spread-operatorn:
Implementering
function union (sets) {
return sets.reduce((combined, list) => {
return new Set([...combined, ...list]);
}, new Set());
}
Användning:
const a = new Set([1, 2, 3]);
const b = new Set([1, 3, 5]);
const c = new Set([4, 5, 6]);
union([a, b, c]) // {1, 2, 3, 4, 5, 6}
Tips:
Vi kan också använda rest-operatorn för att göra gränssnittet lite trevligare:
function union (...sets) {
return sets.reduce((combined, list) => {
return new Set([...combined, ...list]);
}, new Set());
}
Nu, istället för att skicka en array av mängder, kan vi skicka ett godtyckligt antal argument av mängder:
union(a, b, c) // {1, 2, 3, 4, 5, 6}
