Tìm kiếm trang web

javascript - Cách đơn giản nhất để gộp ES6 Maps/Sets?


Có cách đơn giản nào để gộp các Map trong ES6 lại với nhau (như Object.assign) không? Và nhân tiện, còn Set trong ES6 thì sao (như Array.concat)?

Câu trả lời #1

Đối với các tập hợp:

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

Đối với bản đồ:

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

Lưu ý rằng nếu nhiều bản đồ có cùng một khóa, giá trị của bản đồ được hợp nhất sẽ là giá trị của bản đồ được hợp nhất cuối cùng có khóa đó.

Câu trả lời #2

Vì những lý do mà tôi không hiểu, bạn không thể trực tiếp thêm nội dung của một Set vào Set khác bằng một phương thức tích hợp sẵn. Các thao tác như hợp, giao, gộp, v.v... là những thao tác tập cơ bản, nhưng không được tích hợp sẵn. May mắn thay, bạn có thể tự xây dựng tất cả những điều này khá dễ dàng (xem mã dưới đây).


[Đã thêm vào năm 2021] - Hiện nay có một đề xuất để thêm các phương thức Set/Map mới cho các loại thao tác này, nhưng thời gian triển khai vẫn chưa rõ ràng. Chúng dường như đang ở Giai đoạn 2 (Giai đoạn Dự thảo) của quá trình đặc tả.

[Được thêm vào năm 2023] - Đề xuất hiện đang ở Giai đoạn 3 (Giai đoạn ứng viên) của quy trình đặc tả.

[Đã thêm vào tháng 6 năm 2024] - Hiện đang có các triển khai của đề xuất này được phát hành trên Chrome 122, Edge (không chắc phiên bản nào), Firefox 127, Safari 17.0 và Nodejs 23.0.0.


Để tự thực hiện một thao tác gộp (gộp nội dung của một Set vào Set khác hoặc một Map vào Map khác), bạn có thể làm điều này chỉ với một dòng .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

Và, đối với một Map, bạn có thể làm điều này:

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

Hoặc, trong cú pháp ES6:

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

[Được thêm vào năm 2021]

Vì hiện nay đã có một đề xuất chính thức cho các phương thức mới của Set, bạn có thể sử dụng polyfill này cho phương thức .union() được đề xuất, sẽ hoạt động trong các phiên bản ES6+ của ECMAScript. Lưu ý, theo đặc tả, phương thức này trả về một Set mới là hợp của hai Set khác. Nó không hợp nhất nội dung của một Set vào Set khác và điều này thực hiện việc kiểm tra kiểu dữ liệu được chỉ định trong đề xuất.

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

Hoặc, đây là phiên bản đầy đủ hơn mô phỏng quá trình ECMAScript để lấy constructor của species một cách toàn diện hơn và đã được điều chỉnh để chạy trên các phiên bản JavaScript cũ hơn mà có thể không có Symbol hoặc không có Symbol.species được thiết lập:

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

Thông tin cho bạn, nếu bạn muốn một lớp con đơn giản của đối tượng Set tích hợp có chứa một phương thức .merge(), bạn có thể sử dụng cái này:

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

Điều này dự định để ở trong chính tệp của nó setex.js mà bạn sau đó có thể require() vào node.js và sử dụng thay cho Set tích hợp sẵn.

Câu trả lời #3

Đây là giải pháp của tôi sử dụng các generator:

Đối với Bản đồ:

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

Đối với các Bộ:

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

Câu trả lời #4

Chrome 122 và Safari 17 ra mắt các phương thức Set mới (tài liệu MDN, bảng caniuse):

const union = setA.union(setB);

Điều đó thật tuyệt nhưng điều đó tạo ra một tập hợp mới mỗi lần.

Nếu bạn muốn biến đổi một đối tượng hiện có, hãy sử dụng một hàm trợ giúp:

Bộ

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

Cách sử dụng :

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

Bản đồ

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

Cách sử dụng :

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]

Câu trả lời #5

Chỉnh sửa:

Tôi đã so sánh giải pháp gốc của mình với các giải pháp khác được gợi ý ở đây và nhận thấy rằng nó rất kém hiệu quả.

Chính bản chuẩn mực này rất thú vị (liên kết) Nó so sánh 3 giải pháp (cao hơn là tốt hơn):

  • Giải pháp của @fregante (trước đây gọi là @bfred.it), thêm các giá trị từng cái một (14.955 phép toán/giây)
  • Giải pháp của @jameslk, sử dụng một generator tự gọi (5.089 op/giây)
  • của riêng tôi, sử dụng reduce & spread (3.434 op/giây)

Như bạn có thể thấy, giải pháp của @fregante chắc chắn là người chiến thắng.

Hiệu suất + Tính bất biến

Với điều đó trong tâm trí, đây là phiên bản được chỉnh sửa một chút không làm thay đổi tập ban đầu và nhận một số lượng biến đổi các iterable để kết hợp làm đối số:

function union(...iterables) {
  const set = new Set();

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

  return set;
}

Cách sử dụng:

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}

Câu trả lời gốc

Tôi muốn đề xuất một cách tiếp cận khác, sử dụng reduce và toán tử spread:

Triển khai

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

Cách sử dụng:

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}

Mẹo:

Chúng ta cũng có thể sử dụng toán tử rest để làm cho giao diện đẹp hơn một chút:

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

Bây giờ, thay vì truyền một mảng các tập hợp, chúng ta có thể truyền một số tham số tùy ý của các tập hợp:

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

Bài viết liên quan