JavaScript 세트의 객체 동등성을 사용자 정의하는 방법


167

New ES 6 (Harmony)에 새로운 Set 객체가 도입되었습니다 . Set에서 사용하는 ID 알고리즘은 ===연산자와 유사 하므로 객체를 비교하는 데 적합하지 않습니다.

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

심도있는 오브젝트 비교를 위해 Set 오브젝트에 대해 동등성을 사용자 정의하는 방법은 무엇입니까? Java와 같은 것이 equals(Object)있습니까?


3
"평등 사용자 정의"란 무엇을 의미합니까? Javascript는 연산자 오버로드를 허용하지 않으므로 연산자를 오버로드 할 방법이 없습니다 ===. ES6 세트 오브젝트에는 비교 메소드가 없습니다. .has()방법 .add()만이 프리미티브에 대해 동일한 실제 객체 또는 동일한 값이되는 워크 오프 법.
jfriend00

12
"균등 사용자 정의"란 개발자가 특정 몇 가지 객체를 동일하게 간주 할 수있는 방법을 의미합니다.
czerny

답변:


107

ES6 Set객체에는 비교 방법이나 사용자 지정 비교 확장 성이 없습니다.

.has(), .add().delete()방법은 그 프리미티브에 대해 동일한 실제 객체 또는 동일한 가치 오프 동작과에 연결하는 수단을 가지고 아니면 논리 것으로 대체하지 않는다.

당신은 아마도에서 자신의 개체를 도출 할 수 Set및 교체 .has(), .add().delete()항목 설정에 이미있는 경우 깊은 개체를 비교 한 뭔가 방법을 먼저 찾아야하지만, 기본부터 성능은 가능성이 좋지 않을 것 Set오브젝트가 도움이되지 않을 것이다 조금도. original을 호출하기 전에 사용자 정의 비교를 사용하여 일치하는 항목을 찾으려면 기존의 모든 객체를 통해 무차별 강제 반복을 수행해야 할 것입니다 .add().

이 기사의 정보 ES6 기능에 대한 토론 은 다음과 같습니다 .

5.2 맵과 세트가 키와 값을 비교하는 방법을 구성 할 수없는 이유는 무엇입니까?

질문 : 어떤 맵 키와 동일한 설정 요소를 구성 할 수있는 방법이 있다면 좋을 것입니다. 왜 없습니까?

답변 :이 기능은 적절하고 효율적으로 구현하기 어렵 기 때문에 연기되었습니다. 한 가지 옵션은 콜백을 동등성을 지정하는 컬렉션으로 전달하는 것입니다.

Java에서 사용할 수있는 또 다른 옵션은 객체가 구현하는 메소드 (Java의 equals ())를 통해 동등성을 지정하는 것입니다. 그러나이 방법은 변경 가능한 객체에 문제가 있습니다. 일반적으로 객체가 변경되면 컬렉션 내부의 "위치"도 변경되어야합니다. 그러나 그것은 Java에서 일어나는 일이 아닙니다. 자바 스크립트는 아마도 불변의 특수 객체 (값 객체)의 값으로 만 비교할 수있는 안전한 경로 일 것입니다. 값을 기준으로 비교하면 내용이 동일한 경우 두 값이 동일한 것으로 간주됩니다. 기본 값은 JavaScript에서 값으로 비교됩니다.


4
이 특정 문제에 대한 기사 참조가 추가되었습니다. 문제는 세트에 추가되었을 때 다른 객체와 정확히 같지만 이제 변경되어 더 이상 해당 객체와 동일하지 않은 객체를 처리하는 방법입니다. 그것은에서인가 Set여부?
jfriend00

3
간단한 GetHashCode 또는 이와 유사한 것을 구현하지 않는 이유는 무엇입니까?
Jamby

@ Jamby-모든 유형의 속성을 처리하고 올바른 순서로 속성을 해시하고 순환 참조 등을 처리하는 해시를 만드는 재미있는 프로젝트입니다.
jfriend00

1
@Jamby 해시 함수를 사용하더라도 여전히 충돌을 처리해야합니다. 당신은 평등 문제를 연기하고 있습니다.
mpen

5
@mpen 맞지 않습니다. 개발자가 객체의 특성을 알고 좋은 키를 도출 할 수 있기 때문에 거의 모든 경우 충돌 문제를 방지하는 특정 클래스에 대한 자체 해시 함수를 관리 할 수 ​​있습니다. 다른 경우에는 현재 비교 방법으로 대체하십시오. 부지 언어는 이미 , JS을 그렇게하지 않습니다.
Jamby

28

jfriend00의 답변 에서 언급했듯이 평등 관계의 사용자 정의는 아마도 불가능합니다 .

다음 코드는 계산이 효율적이지만 메모리 비용이 많이 드는 해결 방법에 대한 개요를 제공합니다 .

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

    // ...
}

삽입 된 각 요소는 toIdString()문자열을 반환 하는 메서드 를 구현 해야합니다. toIdString메소드가 동일한 값을 리턴하는 경우에만 두 오브젝트가 동일한 것으로 간주됩니다 .


생성자가 항목의 동등성을 비교하는 함수를 사용하도록 할 수도 있습니다. 이 동등성이 세트에 사용 된 오브젝트가 아닌 세트의 기능이되도록하려면 좋습니다.
벤 J

1
@BenJ 문자열을 생성하여 맵에 넣는 요점은 자바 스크립트 엔진이 객체의 해시 값을 검색하기 위해 네이티브 코드에서 ~ O (1) 검색을 사용하지만 등식 함수를 허용하면 세트의 선형 스캔을 수행하고 모든 요소를 ​​확인하십시오.
Jamby

3
이 방법의 한 가지 과제는 값 item.toIdString()이 변하지 않으며 변경할 수 없다고 가정한다는 것 입니다. 가능하면 GeneralSet"중복 된"항목으로 쉽게 무효화 될 수 있기 때문입니다. 따라서 이와 같은 솔루션은 세트를 사용하는 동안 객체 자체가 변경되지 않거나 유효하지 않은 세트가 결과가 아닌 특정 상황으로 만 제한됩니다. 이러한 모든 문제는 아마도 특정 상황에서만 작동하기 때문에 ES6 세트가이 기능을 공개하지 않는 이유를 더 설명 할 것입니다.
jfriend00

.delete()이 답변에 올바른 구현을 추가 할 수 있습니까?
jlewkovich

1
@JLewkovich 확실한
czerny

6

최고 답변에서 언급 했듯이 등식 사용자 정의는 가변 객체에 문제가 있습니다. 좋은 소식은 (아직 아무도 이것을 언급하지 않은 것에 놀랐습니다) immutable-js 라는 매우 인기있는 라이브러리가 있습니다.이 라이브러리는 원하는 불변 값 의미 의미 를 제공하는 불변 유형 유형이 풍부 합니다.

다음은 immutable-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
immutable-js Set / Map의 성능은 기본 Set / Map과 어떻게 비교됩니까?
Frankster

5

여기에 답변을 추가하기 위해 사용자 정의 해시 함수, 사용자 정의 평등 함수를 사용하고 버킷에 동등한 (사용자 정의) 해시가있는 고유 값을 저장하는 맵 래퍼를 구현했습니다.

예상대로 czerny의 문자열 연결 방법 보다 느립니다 .

여기에 전체 소스 : https://github.com/makoConstruct/ValueMap


"문자열 연결"? 그의 방법은 "문자열 대리"와 비슷하지 않습니까 (이름을 주려는 경우)? 아니면“연결”이라는 단어를 사용하는 이유가 있습니까? 궁금하다 ;-)
binki

@ binki 이것은 좋은 질문이며 그 대답은 이해하는 데 시간이 걸렸다는 좋은 지적이 있다고 생각합니다. 일반적으로 해시 코드를 계산할 때 개별 필드의 해시 코드를 곱한 고유 한 것으로 보장되지 않는 HashCodeBuilder 와 같은 작업 을 수행합니다 (따라서 사용자 정의 동등 함수 필요). 그러나 id 문자열을 생성 할 때 고유 한 것으로 보장되는 개별 필드의 id 문자열을 연결합니다 (따라서 등식 함수가 필요하지 않음)
Pace

당신이 경우 지금 Point과 같이 정의 { x: number, y: number }다음 id string아마 x.toString() + ',' + y.toString().
페이스

평등 비교가 평등으로 간주되어야 할 때만 변할 수있는 가치를 구축하는 것은 내가 전에 사용했던 전략입니다. 가끔 그런 식으로 생각하는 것이 더 쉽습니다. 이 경우 해시 대신 키를 생성 합니다 . 기존 도구가 가치 스타일 평등을 지원하는 형태로 키를 출력하는 키 디 리버가있는 한 거의 항상 끝나는 String것처럼 말한대로 전체 해싱 및 버킷 팅 단계를 건너 뛸 수 있으며 직접 Map또는 파생 키 측면에서 구식 일반 객체입니다.
binki

1
키 디 라이버를 구현할 때 실제로 문자열 연결을 사용하는 경우주의해야 할 사항은 문자열 속성이 값을 가질 수있는 경우 특수하게 처리해야한다는 것입니다. 예를 들어, 당신은 할 경우 {x: '1,2', y: '3'}{x: '1', y: '2,3'}다음, String(x) + ',' + String(y)두 객체를 출력 같은 값입니다. JSON.stringify()결정 론적 이라고 생각하면 안전한 옵션 은 문자열 이스케이프를 이용하고 JSON.stringify([x, y])대신 사용하는 것입니다.
binki

3

직접 비교하는 것은 불가능한 것처럼 보이지만 키가 방금 정렬 된 경우 JSON.stringify가 작동합니다. 의견에서 지적했듯이

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

그러나 사용자 정의 stringify 메소드를 사용하여 해결할 수 있습니다. 먼저 우리는 방법을 작성

맞춤 문자열 화

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

세트

이제 우리는 Set을 사용합니다. 그러나 객체 대신 문자열 집합을 사용합니다.

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

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

모든 가치를 얻으십시오

세트를 생성하고 값을 추가 한 후 모든 값을 얻을 수 있습니다.

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

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

하나의 파일에 모두 링크가 있습니다 http://tpcg.io/FnJg2i


"키가 정렬 된 경우"는 특히 복잡한 개체의 경우 큰 것입니다
Alexander Mills

그것이 바로 내가이 접근법을 선택한 이유입니다.)
relief.melone

2

JSON.stringify()심도있는 객체 비교를 위해 사용할 수 있습니다 .

예를 들면 다음과 같습니다.

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
이것은 작동 할 수 있지만 JSON.stringify ({a : 1, b : 2})와 같을 필요는 없습니다.! == JSON.stringify ({b : 2, a : 1}) 모든 객체가 프로그램에 의해 안전하다고 주문하십시오. 일반적으로하지만 정말 안전한 솔루션
relief.melone

1
예, "문자열로 변환하십시오". 모든 것에 대한 자바 스크립트의 대답.
Timmmm

2

Typescript 사용자의 경우 다른 사람 (특히 czerny ) 의 답변을 멋진 유형 안전하고 재사용 가능한 기본 클래스로 일반화 할 수 있습니다.

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

구현 예제는 다음과 같습니다 stringifyKey. 메소드를 재정의하십시오 . 내 경우에는 일부 uri속성을 지정합니다.

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

이 예제는 일반적인 것처럼 사용합니다 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

두 세트의 조합으로 새 세트를 만든 다음 길이를 비교하십시오.

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은 set2 = true와 같습니다.

set1은 set4 = false와 같습니다.


2
이 답변은 질문과 관련이 있습니까? 문제는 Set 클래스의 인스턴스와 관련하여 항목의 동등성에 관한 것입니다. 이 질문은 두 Set 인스턴스의 동등성을 논의하는 것 같습니다.
czerny

@czerny 당신이 맞습니다-원래 위의 방법을 사용할 수있는이 stackoverflow 질문을보고있었습니다 : stackoverflow.com/questions/6229197/…
Stefan Musarra

-2

Google에서이 질문을 발견 한 사람에게 객체를 키로 사용하여지도의 가치를 얻으려는 경우 :

경고 : 이 답변은 모든 객체에서 작동하지는 않습니다

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

산출:

일한

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.