Cách tùy chỉnh bình đẳng đối tượng cho Bộ JavaScript


167

ES 6 mới (Harmony) giới thiệu đối tượng Set mới . Thuật toán nhận dạng được Set sử dụng tương tự như ===toán tử và do đó không phù hợp lắm để so sánh các đối tượng:

var set = new Set();
set.add({a:1});
set.add({a:1});
console.log([...set.values()]); // Array [ Object, Object ]

Làm cách nào để tùy chỉnh đẳng thức cho Đặt đối tượng để thực hiện so sánh đối tượng sâu? Có bất cứ điều gì như Java equals(Object)?


3
Bạn có ý nghĩa gì bởi "tùy chỉnh bình đẳng"? Javascript không cho phép quá tải toán tử nên không có cách nào để quá tải ===toán tử. Đối tượng thiết lập ES6 không có bất kỳ phương thức so sánh nào. Các .has()phương pháp và .add()phương pháp làm việc chỉ ra nó là đối tượng thực tế tương tự hoặc cùng một giá trị cho một nguyên thủy.
jfriend00

12
Bằng cách "tùy chỉnh sự bình đẳng" Tôi có nghĩa là bất kỳ cách nào nhà phát triển có thể định nghĩa một số đối tượng nhất định được coi là bằng nhau hay không.
czerny

Câu trả lời:


107

SetĐối tượng ES6 không có bất kỳ phương thức so sánh hoặc khả năng mở rộng so sánh tùy chỉnh nào.

Các .has(), .add().delete()phương pháp làm việc chỉ ra nó là đối tượng thực tế tương tự hoặc cùng một giá trị cho một nguyên thủy và không có một phương tiện để cắm vào hoặc thay thế chỉ là logic.

Bạn có thể lấy được đối tượng của chính mình từ một Setvà thay thế .has(), .add().delete()các phương thức với một thứ đã so sánh đối tượng sâu trước để tìm xem vật phẩm đã có trong Bộ chưa, nhưng hiệu suất có thể không tốt vì Setđối tượng cơ bản sẽ không giúp ích ở tất cả. Bạn có thể chỉ cần thực hiện một bước lặp mạnh mẽ thông qua tất cả các đối tượng hiện có để tìm một kết quả khớp bằng cách sử dụng so sánh tùy chỉnh của riêng bạn trước khi gọi bản gốc .add().

Dưới đây là một số thông tin từ bài viết này và thảo luận về các tính năng ES6:

5.2 Tại sao tôi không thể định cấu hình cách bản đồ và bộ so sánh các khóa và giá trị?

Câu hỏi: Sẽ thật tuyệt nếu có một cách để định cấu hình các khóa bản đồ và các thành phần nào được coi là bằng nhau. Tại sao không có ở đó?

Trả lời: Tính năng đó đã bị hoãn lại, vì rất khó để thực hiện đúng và hiệu quả. Một lựa chọn là trao các cuộc gọi lại cho các bộ sưu tập xác định sự bằng nhau.

Một tùy chọn khác, có sẵn trong Java, là chỉ định đẳng thức thông qua một phương thức mà đối tượng triển khai (bằng () trong Java). Tuy nhiên, cách tiếp cận này có vấn đề đối với các đối tượng có thể thay đổi: Nói chung, nếu một đối tượng thay đổi, thì vị trí của nó trong một bộ sưu tập cũng phải thay đổi. Nhưng đó không phải là những gì xảy ra trong Java. JavaScript có thể sẽ đi theo con đường an toàn hơn khi chỉ cho phép so sánh theo giá trị cho các đối tượng bất biến đặc biệt (được gọi là các đối tượng giá trị). So sánh theo giá trị có nghĩa là hai giá trị được coi là bằng nhau nếu nội dung của chúng bằng nhau. Các giá trị nguyên thủy được so sánh theo giá trị trong JavaScript.


4
Đã thêm bài viết tham khảo về vấn đề đặc biệt này. Có vẻ như thách thức là làm thế nào để đối phó với một đối tượng giống hệt như một đối tượng khác tại thời điểm nó được thêm vào tập hợp, nhưng giờ đã được thay đổi và không còn giống với đối tượng đó nữa. Có phải trong Sethay không?
jfriend00

3
Tại sao không thực hiện một GetHashCode đơn giản hoặc tương tự?
Jamby

@Jamby - Đó sẽ là một dự án thú vị để tạo ra một hàm băm xử lý tất cả các loại thuộc tính và băm thuộc tính theo đúng thứ tự và xử lý các tham chiếu vòng tròn, v.v.
jfriend00

1
@Jamby Ngay cả với hàm băm bạn vẫn phải xử lý va chạm. Bạn chỉ đang trì hoãn vấn đề bình đẳng.
mở

5
@mpen Điều đó không đúng, tôi cho phép nhà phát triển quản lý hàm băm của riêng mình cho các lớp cụ thể của mình, trong hầu hết mọi trường hợp đều ngăn chặn sự cố va chạm do nhà phát triển biết bản chất của các đối tượng và có thể lấy được khóa tốt. Trong mọi trường hợp khác, dự phòng cho phương pháp so sánh hiện tại. của ngôn ngữ đã làm điều đó, js không.
Jamby

28

Như đã đề cập trong tùy chỉnh câu trả lời của jfriend00 về mối quan hệ bình đẳng có lẽ là không thể .

Đoạn mã sau trình bày một phác thảo về cách giải quyết tính toán hiệu quả (nhưng đắt bộ nhớ) :

class GeneralSet {

    constructor() {
        this.map = new Map();
        this[Symbol.iterator] = this.values;
    }

    add(item) {
        this.map.set(item.toIdString(), item);
    }

    values() {
        return this.map.values();
    }

    delete(item) {
        return this.map.delete(item.toIdString());
    }

    // ...
}

Mỗi phần tử được chèn phải thực hiện toIdString()phương thức trả về chuỗi. Hai đối tượng được coi là bằng nhau khi và chỉ khi toIdStringphương thức của chúng trả về cùng một giá trị.


Bạn cũng có thể yêu cầu hàm tạo lấy một hàm so sánh các mục cho bằng nhau. Điều này là tốt nếu bạn muốn sự bình đẳng này là một tính năng của tập hợp, chứ không phải là các đối tượng được sử dụng trong nó.
Ben J

1
@BenJ Điểm tạo chuỗi và đặt chuỗi vào Bản đồ là theo cách đó, công cụ Javascript của bạn sẽ sử dụng tìm kiếm ~ O (1) trong mã gốc để tìm kiếm giá trị băm của đối tượng của bạn, trong khi chấp nhận hàm bình đẳng sẽ buộc để thực hiện quét tuyến tính của tập hợp và kiểm tra mọi phần tử.
Jamby

3
Một thách thức với phương pháp này là tôi nghĩ rằng nó giả định rằng giá trị của item.toIdString()là bất biến và không thể thay đổi. Bởi vì nếu có thể, thì nó GeneralSetcó thể dễ dàng trở thành không hợp lệ với các mục "trùng lặp" trong đó. Vì vậy, một giải pháp như thế sẽ bị hạn chế chỉ trong một số tình huống có khả năng bản thân các đối tượng không bị thay đổi trong khi sử dụng bộ hoặc khi một bộ trở nên không hợp lệ không phải là hậu quả. Tất cả các vấn đề này có thể giải thích thêm tại sao Bộ ES6 không phơi bày chức năng này vì nó thực sự chỉ hoạt động trong một số trường hợp nhất định.
jfriend00

Có thể thêm việc thực hiện chính xác .delete()cho câu trả lời này?
jlewkovich

1
@JLewkovich chắc chắn
czerny

6

Như câu trả lời hàng đầu đề cập, việc tùy chỉnh bình đẳng là vấn đề đối với các đối tượng có thể thay đổi. Tin tốt là (và tôi ngạc nhiên là chưa có ai đề cập đến vấn đề này) có một thư viện rất nổi tiếng gọi là bất biến-js cung cấp một tập hợp phong phú các loại bất biến cung cấp ngữ nghĩa bình đẳng giá trị sâu sắc mà bạn đang tìm kiếm.

Đây là ví dụ của bạn bằng cách sử dụng bất biến-js :

const { Map, Set } = require('immutable');
var set = new Set();
set = set.add(Map({a:1}));
set = set.add(Map({a:1}));
console.log([...set.values()]); // [Map {"a" => 1}]

10
Hiệu suất của Set / Map bất biến so với Set / Map gốc như thế nào?
frankster

5

Để thêm vào câu trả lời ở đây, tôi đã tiếp tục và triển khai trình bao bọc Bản đồ có hàm băm tùy chỉnh, hàm bình đẳng tùy chỉnh và lưu trữ các giá trị riêng biệt có băm tương đương (tùy chỉnh) trong các thùng.

Có thể dự đoán, hóa ra là chậm hơn so với phương pháp nối chuỗi của czerny .

Nguồn đầy đủ tại đây: https://github.com/makoConstruct/ValueMap


Sê-ri chuỗi liên kết Không phải phương pháp của anh ấy giống như chuỗi hơn thay thế tên lửa (nếu bạn định đặt tên cho nó)? Hay có một lý do nào khiến bạn sử dụng từ "concatenation"? Tôi tò mò ;-)
binki

@binki Đây là một câu hỏi hay và tôi nghĩ rằng câu trả lời mang đến một điểm tốt mà tôi phải mất một thời gian để nắm bắt. Thông thường, khi tính toán mã băm, người ta sẽ làm một cái gì đó như HashCodeBuilder để nhân mã băm của các trường riêng lẻ và không được đảm bảo là duy nhất (do đó cần có hàm bình đẳng tùy chỉnh). Tuy nhiên, khi tạo một chuỗi id, bạn nối các chuỗi id của các trường riêng lẻ được đảm bảo là duy nhất (và do đó không cần chức năng bình đẳng)
Pace

Vì vậy, nếu bạn có một Pointđịnh nghĩa như vậy { x: number, y: number }thì id stringcó lẽ bạn là x.toString() + ',' + y.toString().
Pace

Làm cho so sánh bình đẳng của bạn xây dựng một số giá trị được đảm bảo chỉ thay đổi khi mọi thứ nên được coi là không bằng nhau là một chiến lược tôi đã sử dụng trước đây. Đôi khi nghĩ về những điều đó dễ dàng hơn. Trong trường hợp đó, bạn đang tạo khóa chứ không phải băm . Miễn là bạn có một công cụ tạo khóa chính tạo ra một khóa ở dạng mà các công cụ hiện có hỗ trợ với sự bình đẳng theo kiểu giá trị, hầu như luôn luôn kết thúc String, thì bạn có thể bỏ qua toàn bộ bước băm và xô như bạn đã nói và chỉ cần sử dụng trực tiếp Maphoặc thậm chí đối tượng đơn giản kiểu cũ về khóa dẫn xuất.
binki

1
Một điều cần cẩn thận nếu bạn thực sự sử dụng phép nối chuỗi trong quá trình thực hiện một bộ tách khóa là các thuộc tính chuỗi có thể cần được xử lý đặc biệt nếu chúng được phép nhận bất kỳ giá trị nào. Ví dụ: nếu bạn có {x: '1,2', y: '3'}{x: '1', y: '2,3'}, sau đó String(x) + ',' + String(y)sẽ xuất cùng một giá trị cho cả hai đối tượng. Một tùy chọn an toàn hơn, giả sử bạn có thể tin tưởng vào tính JSON.stringify()xác định, là tận dụng chuỗi thoát của nó và sử dụng JSON.stringify([x, y])thay thế.
biley

3

So sánh chúng trực tiếp dường như là không thể, nhưng JSON.opesify hoạt động nếu các khóa vừa được sắp xếp. Như tôi đã chỉ ra trong một bình luận

JSON.opesify ({a: 1, b: 2})! == JSON.opesify ({b: 2, a: 1});

Nhưng chúng ta có thể làm việc xung quanh đó với một phương thức chuỗi tùy chỉnh. Đầu tiên chúng ta viết phương thức

Stringify tùy chỉnh

Object.prototype.stringifySorted = function(){
    let oldObj = this;
    let obj = (oldObj.length || oldObj.length === 0) ? [] : {};
    for (let key of Object.keys(this).sort((a, b) => a.localeCompare(b))) {
        let type = typeof (oldObj[key])
        if (type === 'object') {
            obj[key] = oldObj[key].stringifySorted();
        } else {
            obj[key] = oldObj[key];
        }
    }
    return JSON.stringify(obj);
}

Bộ

Bây giờ chúng tôi sử dụng một bộ. Nhưng chúng tôi sử dụng một bộ Chuỗi thay vì các đối tượng

let set = new Set()
set.add({a:1, b:2}.stringifySorted());

set.has({b:2, a:1}.stringifySorted());
// returns true

Nhận tất cả các giá trị

Sau khi chúng ta tạo tập hợp và thêm các giá trị, chúng ta có thể nhận được tất cả các giá trị bằng cách

let iterator = set.values();
let done = false;
while (!done) {
  let val = iterator.next();

  if (!done) {
    console.log(val.value);
  }
  done = val.done;
}

Đây là một liên kết với tất cả trong một tệp http://tpcg.io/FnJg2i


"nếu các khóa được sắp xếp" là lớn nếu, đặc biệt là đối với các đối tượng phức tạp
Alexander Mills

đó chính xác là lý do tại sao tôi chọn cách tiếp cận này;)
cứu trợ.melone

2

Có lẽ bạn có thể thử sử dụng JSON.stringify()để làm so sánh đối tượng sâu.

ví dụ :

const arr = [
  {name:'a', value:10},
  {name:'a', value:20},
  {name:'a', value:20},
  {name:'b', value:30},
  {name:'b', value:40},
  {name:'b', value:40}
];

const names = new Set();
const result = arr.filter(item => !names.has(JSON.stringify(item)) ? names.add(JSON.stringify(item)) : false);

console.log(result);


2
Điều này có thể hoạt động nhưng không phải là JSON.opesify ({a: 1, b: 2})! == JSON.opesify ({b: 2, a: 1}) Nếu tất cả các đối tượng được tạo bởi chương trình của bạn trong cùng đặt hàng bạn an toàn. Nhưng không phải là một giải pháp thực sự an toàn nói chung
cứu trợ.melone 18/11/18

1
À đúng rồi, "chuyển đổi nó thành một chuỗi". Câu trả lời của Javascript cho mọi thứ.
Timmmm

2

Đối với người dùng Bản mô tả, các câu trả lời của người khác (đặc biệt là czerny ) có thể được khái quát thành một lớp cơ sở an toàn và có thể tái sử dụng loại tốt:

/**
 * Map that stringifies the key objects in order to leverage
 * the javascript native Map and preserve key uniqueness.
 */
abstract class StringifyingMap<K, V> {
    private map = new Map<string, V>();
    private keyMap = new Map<string, K>();

    has(key: K): boolean {
        let keyString = this.stringifyKey(key);
        return this.map.has(keyString);
    }
    get(key: K): V {
        let keyString = this.stringifyKey(key);
        return this.map.get(keyString);
    }
    set(key: K, value: V): StringifyingMap<K, V> {
        let keyString = this.stringifyKey(key);
        this.map.set(keyString, value);
        this.keyMap.set(keyString, key);
        return this;
    }

    /**
     * Puts new key/value if key is absent.
     * @param key key
     * @param defaultValue default value factory
     */
    putIfAbsent(key: K, defaultValue: () => V): boolean {
        if (!this.has(key)) {
            let value = defaultValue();
            this.set(key, value);
            return true;
        }
        return false;
    }

    keys(): IterableIterator<K> {
        return this.keyMap.values();
    }

    keyList(): K[] {
        return [...this.keys()];
    }

    delete(key: K): boolean {
        let keyString = this.stringifyKey(key);
        let flag = this.map.delete(keyString);
        this.keyMap.delete(keyString);
        return flag;
    }

    clear(): void {
        this.map.clear();
        this.keyMap.clear();
    }

    size(): number {
        return this.map.size;
    }

    /**
     * Turns the `key` object to a primitive `string` for the underlying `Map`
     * @param key key to be stringified
     */
    protected abstract stringifyKey(key: K): string;
}

Ví dụ thực hiện là đơn giản: chỉ cần ghi đè stringifyKey phương thức. Trong trường hợp của tôi, tôi xâu chuỗi một số uritài sản.

class MyMap extends StringifyingMap<MyKey, MyValue> {
    protected stringifyKey(key: MyKey): string {
        return key.uri.toString();
    }
}

Ví dụ sử dụng là như thể đây là một thường xuyên Map<K, V>.

const key1 = new MyKey(1);
const value1 = new MyValue(1);
const value2 = new MyValue(2);

const myMap = new MyMap();
myMap.set(key1, value1);
myMap.set(key1, value2); // native Map would put another key/value pair

myMap.size(); // returns 1, not 2

-1

Tạo một bộ mới từ sự kết hợp của cả hai bộ, sau đó so sánh độ dài.

let set1 = new Set([1, 2, 'a', 'b'])
let set2 = new Set([1, 'a', 'a', 2, 'b'])
let set4 = new Set([1, 2, 'a'])

function areSetsEqual(set1, set2) {
  const set3 = new Set([...set1], [...set2])
  return set3.size === set1.size && set3.size === set2.size
}

console.log('set1 equals set2 =', areSetsEqual(set1, set2))
console.log('set1 equals set4 =', areSetsEqual(set1, set4))

set1 bằng set2 = true

set1 bằng set4 = false


2
Là câu trả lời này liên quan đến câu hỏi? Câu hỏi là về sự bình đẳng của các mục liên quan đến một thể hiện của lớp Set. Câu hỏi này dường như thảo luận về sự bình đẳng của hai trường hợp Set.
czerny

@czerny bạn là chính xác - i ban đầu được xem câu hỏi này stackoverflow, mà phương pháp trên có thể được sử dụng: stackoverflow.com/questions/6229197/...
Stefan Musarra

-2

Đối với ai đó đã tìm thấy câu hỏi này trên Google (như tôi) muốn nhận giá trị của Bản đồ bằng cách sử dụng một đối tượng làm Khóa:

Cảnh báo: câu trả lời này sẽ không hoạt động với tất cả các đối tượng

var map = new Map<string,string>();

map.set(JSON.stringify({"A":2} /*string of object as key*/), "Worked");

console.log(map.get(JSON.stringify({"A":2}))||"Not worked");

Đầu ra:

Đã làm việc

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.