【问题标题】:Simplest way to merge ES6 Maps/Sets?合并 ES6 地图/集的最简单方法?
【发布时间】:2015-11-07 04:00:19
【问题描述】:

有没有一种简单的方法可以将 ES6 Maps 合并在一起(比如 Object.assign)?在我们讨论的时候,ES6 Sets(比如Array.concat)呢?

【问题讨论】:

  • 这篇博文有一些见解。 2ality.com/2015/01/es6-set-operations.html
  • AFAIK 用于 Map 您需要使用 for..of,因为 key 可以是任何类型
  • 我在这里尝试了几个较高的建议,但没有成功。 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… 提供的“联合”功能运行良好。作为单行,这里(注意:作为单行,我需要在“for”语句之前添加一个分号):function union(setA, setB) { let _union = new Set(setA); for (let elem of setB) { _union.add(elem) } return _union }; set1 = new Set(['apple']); set2 = new Set(['banana']); set3 = union(set1,set2); console.log(set3) // Set [ "apple", "banana" ]

标签: javascript set ecmascript-6


【解决方案1】:

对于套装:

var merged = new Set([...set1, ...set2, ...set3])

对于地图:

var merged = new Map([...map1, ...map2, ...map3])

请注意,如果多个映射具有相同的键,则合并映射的值将是具有该键的最后一个合并映射的值。

【讨论】:

  • Map 上的文档:“构造函数:new Map([iterable])”、“iterable 是一个数组或其他元素为键值对(2 元素数组)的可迭代对象。每个键值对都被添加到新的 Map 中。” — 仅供参考。
  • 对于大型集合,请注意,这会迭代两个集合的内容两次,一次是创建一个包含两个集合并集的临时数组,然后将该临时数组传递给它所在的 Set 构造函数再次迭代以创建新的 Set。
  • @jfriend00:请参阅下面 jameslk 的答案以获得更好的方法
  • @torazaburo:正如 jfriend00 所说,Oriols 解决方案确实会创建不必要的中间数组。将迭代器传递给 Map 构造函数可以避免它们的内存消耗。
  • @devuxer 您需要在 tsconfig.json 中启用 compilerOptions.downlevelIteration 以消除编译器错误。见stackoverflow.com/questions/53441292/…
【解决方案2】:

由于我不明白的原因,您不能使用内置方法直接将一个 Set 的内容添加到另一个 Set 中。联合、相交、合并等操作是非常基本的集合操作,但不是内置的。幸运的是,您可以很容易地自己构建这些。

[2021 年添加] - 现在有一个 proposal 为这些类型的操作添加新的 Set/Map 方法,但实施时间尚不清楚。它们似乎处于规范流程的第 2 阶段。

要实现合并操作(将一个 Set 的内容合并到另一个或一个 Map 到另一个),您可以使用单个 .forEach() 行来执行此操作:

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

对于Map,您可以这样做:

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);
});

或者,在 ES6 语法中:

t.forEach((value, key) => s.set(key, value));

[2021 年添加]

由于现在有一个新的 Set 方法的官方提案,您可以将此 polyfill 用于提议的 .union() 方法,该方法适用于 ES6+ 版本的 ECMAScript。请注意,根据规范,这将返回一个新 Set,它是另外两个集合的并集。它不会将一个集合的内容合并到另一个集合中,这实现了proposal 中指定的类型检查。

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;
    }
}

或者,这里有一个更完整的版本,它对 ECMAScript 过程进行建模以更完整地获取物种构造函数,并且已经适应了在甚至可能没有 SymbolSymbol.species 设置的旧版本 Javascript 上运行:

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;
}

仅供参考,如果你想要一个包含 .merge() 方法的内置 Set 对象的简单子类,你可以使用这个:

// 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;

这意味着在它自己的文件 setex.js 中,然后您可以 require() 进入 node.js 并使用它来代替内置的 Set。

【讨论】:

  • 我不认为new Set(s, t)。作品。 t 参数被忽略。此外,让add 检测其参数的类型并且如果集合添加集合的元素显然是不合理的行为,因为那样就无法将集合本身​​添加到集合中。
  • @torazaburo - 至于.add() 的方法,我明白你的意思。我只是发现它比使用.add() 组合集合的用处要少得多,因为我从来没有需要一个或多个集合,但我需要多次合并集合。只是一种行为与另一种行为的有用性的意见问题。
  • 啊,我讨厌这对地图不起作用:n.forEach(m.add, m) - 它确实反转了键/值对!
  • @Bergi - 是的,很奇怪Map.prototype.forEach()Map.prototype.set() 有相反的论点。似乎是某人的疏忽。当尝试将它们一起使用时,它会强制使用更多代码。
  • @jfriend00:OTOH,这有点道理。 set 参数顺序对于键/值对来说是很自然的,forEachArrays forEach 方法(以及类似 $.each_.each 之类的方法也可以枚举对象)对齐。
【解决方案3】:

要合并数组Sets中的集合,可以这样做

var Sets = [set1, set2, set3];

var merged = new Set([].concat(...Sets.map(set => Array.from(set))));

对我来说,为什么以下应该等效的内容至少在 Babel 中失败了,这让我有点神秘:

var merged = new Set([].concat(...Sets.map(Array.from)));

【讨论】:

  • Array.from 采用附加参数,第二个是映射函数。 Array.prototype.map 将三个参数传递给它的回调:(value, index, array),因此它实际上是在调用Sets.map((set, index, array) => Array.from(set, index, array)。显然,index 是一个数字,而不是一个映射函数,所以它会失败。
【解决方案4】:

这是我使用生成器的解决方案:

对于地图:

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' ] ]

对于套装:

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' ]

【讨论】:

  • (IIGFE = 立即调用的生成器函数表达式)
  • 如果你想要简单的浏览器支持也不错m2.forEach((k,v)=>m1.set(k,v))
  • @caub 不错的解决方案,但请记住 forEach 的第一个参数是值,因此您的函数应该是 m2.forEach((v,k)=>m1.set(k,v));跨度>
【解决方案5】:

不,这些没有内置操作,但您可以轻松地自己创建它们:

Map.prototype.assign = function(...maps) {
    for (const m of maps)
        for (const kv of m)
            this.add(...kv);
    return this;
};

Set.prototype.concat = function(...sets) {
    const c = this.constructor;
    let res = new (c[Symbol.species] || c)();
    for (const set of [this, ...sets])
        for (const v of set)
            res.add(v);
    return res;
};

【讨论】:

【解决方案6】:

批准的答案很棒,但每次都会创建一个新集合。

如果您想改变现有对象,请使用辅助函数。

设置

function concatSets(set, ...iterables) {
    for (const iterable of iterables) {
        for (const item of iterable) {
            set.add(item);
        }
    }
}

用法:

const setA = new Set([1, 2, 3]);
const setB = new Set([4, 5, 6]);
const setC = new Set([7, 8, 9]);
concatSets(setA, setB, setC);
// setA will have items 1, 2, 3, 4, 5, 6, 7, 8, 9

地图

function concatMaps(map, ...iterables) {
    for (const iterable of iterables) {
        for (const item of iterable) {
            map.set(...item);
        }
    }
}

用法:

const mapA = new Map().set('S', 1).set('P', 2);
const mapB = new Map().set('Q', 3).set('R', 4);
concatMaps(mapA, mapB);
// mapA will have items ['S', 1], ['P', 2], ['Q', 3], ['R', 4]

【讨论】:

    【解决方案7】:

    示例

    const mergedMaps = (...maps) => {
        const dataMap = new Map([])
    
        for (const map of maps) {
            for (const [key, value] of map) {
                dataMap.set(key, value)
            }
        }
    
        return dataMap
    }
    

    用法

    const map = mergedMaps(new Map([[1, false]]), new Map([['foo', 'bar']]), new Map([['lat', 1241.173512]]))
    Array.from(map.keys()) // [1, 'foo', 'lat']
    

    【讨论】:

      【解决方案8】:

      编辑

      我将我的原始解决方案与此处建议的其他解决方案进行了基准测试,发现它的效率非常低。

      基准测试本身很有趣(link)它比较了 3 个解决方案(越高越好):

      • @fregante(以前称为@bfred.it)解决方案,将值一一相加(14,955 op/sec)
      • @jameslk 的解决方案,它使用自调用生成器 (5,089 op/sec)
      • 我自己的,它使用 reduce & spread (3,434 op/sec)

      如您所见,@fregante 的解决方案绝对是赢家。

      性能 + 不变性

      考虑到这一点,这里有一个稍微修改过的版本,它没有 改变原始集合并排除可变数量的可迭代对象 组合为参数:

      function union(...iterables) {
        const set = new Set();
      
        for (const iterable of iterables) {
          for (const item of iterable) {
            set.add(item);
          }
        }
      
        return set;
      }
      

      用法:

      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}
      

      原答案

      我想建议另一种方法,使用 reducespread 运算符:

      实施

      function union (sets) {
        return sets.reduce((combined, list) => {
          return new Set([...combined, ...list]);
        }, new Set());
      }
      

      用法:

      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}
      

      提示:

      我们还可以使用rest 操作符让界面更漂亮:

      function union (...sets) {
        return sets.reduce((combined, list) => {
          return new Set([...combined, ...list]);
        }, new Set());
      }
      

      现在,我们可以传递任意数量的 arguments 集合,而不是传递集合的 array

      union(a, b, c) // {1, 2, 3, 4, 5, 6}
      

      【讨论】:

      • 嗨@Bergi,你是对的。感谢您提高我的认识(:我已经针对此处建议的其他人测试了我的解决方案并为自己证明了这一点。此外,我已经编辑了我的答案以反映这一点。请考虑删除您的反对票。
      • 太好了,感谢您的性能比较。有趣的是,“不优雅”的解决方案是如何最快的;)来这里是为了寻找对 forofadd 的改进,这似乎效率很低。我真的希望在 Sets 上有一个 addAll(iterable) 方法
      • 打字稿版本:function union<T> (...iterables: Array<Set<T>>): Set<T> { const set = new Set<T>(); iterables.forEach(iterable => { iterable.forEach(item => set.add(item)) }) return set }
      • 答案顶部附近的 jsperf 链接似乎已失效。另外,您参考了 bfred 的解决方案,我在这里看不到任何地方。
      • @jfriend00 当我访问bfred.it 时,我有一个fregante 的推特帐户,所以也许这是fregante 的答案!
      【解决方案9】:

      根据 Asaf Katz 的回答,这是一个打字稿版本:

      export function union<T> (...iterables: Array<Set<T>>): Set<T> {
        const set = new Set<T>()
        iterables.forEach(iterable => {
          iterable.forEach(item => set.add(item))
        })
        return set
      }
      

      【讨论】:

        【解决方案10】:

        将多个元素(来自数组或另一个集合)添加到现有集合时,调用new Set(...anArrayOrSet)没有任何意义

        我在reduce 函数中使用它,这简直是愚蠢的。即使您有可用的 ...array 扩展运算符,您也不应该在这种情况下使用它,因为它会浪费处理器、内存和时间资源。

        // Add any Map or Set to another
        function addAll(target, source) {
          if (target instanceof Map) {
            Array.from(source.entries()).forEach(it => target.set(it[0], it[1]))
          } else if (target instanceof Set) {
            source.forEach(it => target.add(it))
          }
        }
        

        演示片段

        // Add any Map or Set to another
        function addAll(target, source) {
          if (target instanceof Map) {
            Array.from(source.entries()).forEach(it => target.set(it[0], it[1]))
          } else if (target instanceof Set) {
            source.forEach(it => target.add(it))
          }
        }
        
        const items1 = ['a', 'b', 'c']
        const items2 = ['a', 'b', 'c', 'd']
        const items3 = ['d', 'e']
        
        let set
        
        set = new Set(items1)
        addAll(set, items2)
        addAll(set, items3)
        console.log('adding array to set', Array.from(set))
        
        set = new Set(items1)
        addAll(set, new Set(items2))
        addAll(set, new Set(items3))
        console.log('adding set to set', Array.from(set))
        
        const map1 = [
          ['a', 1],
          ['b', 2],
          ['c', 3]
        ]
        const map2 = [
          ['a', 1],
          ['b', 2],
          ['c', 3],
          ['d', 4]
        ]
        const map3 = [
          ['d', 4],
          ['e', 5]
        ]
        
        const map = new Map(map1)
        addAll(map, new Map(map2))
        addAll(map, new Map(map3))
        console.log('adding map to map',
          'keys', Array.from(map.keys()),
          'values', Array.from(map.values()))

        【讨论】:

          【解决方案11】:

          您可以使用spread syntax 将它们合并在一起:

          const map1 = {a: 1, b: 2}
          const map2 = {b: 1, c: 2, a: 5}
          
          const mergedMap = {...a, ...b}
          
          => {a: 5, b: 1, c: 2}
          

          【讨论】:

          • 糟糕,答案错误。 OP 的问题是关于 Map 而不是像 {} 这样的常见对象。
          【解决方案12】:

          将集合转换为数组,将它们展平,最后构造函数将唯一化。

          const union = (...sets) => new Set(sets.map(s => [...s]).flat());
          

          【讨论】:

          • 请不要只发布代码作为答案,还要解释您的代码的作用以及它如何解决问题的问题。带有解释的答案通常质量更高,更有可能吸引投票。
          【解决方案13】:

          我创建了一个小的 sn-p 来使用 ES6 中的函数合并任意数量的 Set。您可以将“Set”更改为“Map”,使其与 Maps 一起使用。

          const mergeSets = (...args) => {
              return new Set(args.reduce((acc, current) => {
                  return [...acc, ...current];
              }, []));
          };
          
          const foo = new Set([1, 2, 3]);
          const bar = new Set([1, 3, 4, 5]);
          
          mergeSets(foo, bar); // Set(5) {1, 2, 3, 4, 5}
          mergeSets(foo, bar, new Set([6])); // Set(6) {1, 2, 3, 4, 5, 6}
          

          【讨论】:

            【解决方案14】:

            无论您是否有两个或多个要合并的地图,一个好的解决方案是将它们分组为一个数组并使用以下内容:

            Array.prototype.merge = function () {
              return this.reduce((p, c) => Object.assign(c, p), {});
            };
            

            【讨论】:

              猜你喜欢
              • 2021-12-13
              • 2014-12-24
              • 2016-05-04
              • 2011-09-02
              • 2017-11-14
              • 2012-11-10
              • 2017-01-19
              • 1970-01-01
              相关资源
              最近更新 更多