วิธีกำหนดความเท่าเทียมกันของวัตถุสำหรับชุด JavaScript


167

ใหม่ ES 6 (Harmony) แนะนำชุดวัตถุใหม่ อัลกอริทึมประจำตัวที่ใช้โดยชุดนั้นคล้ายกับ===โอเปอเรเตอร์และไม่เหมาะสำหรับการเปรียบเทียบวัตถุ:

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

วิธีกำหนดความเท่าเทียมกันสำหรับการตั้งค่าวัตถุเพื่อทำการเปรียบเทียบวัตถุลึก มีอะไรเช่น Java equals(Object)?


3
คุณหมายถึงอะไรโดย "ปรับแต่งความเท่าเทียมกัน"? Javascript ไม่อนุญาตให้มีการใช้งานตัวดำเนินการมากเกินไปดังนั้นจึงไม่มีวิธีที่จะทำให้ตัว===ดำเนินการทำงานหนักเกินไป วัตถุชุด ES6 ไม่มีวิธีการเปรียบเทียบใด ๆ .has()วิธีการและ.add()วิธีการทำงานเท่านั้นปิดมันเป็นวัตถุที่เกิดขึ้นจริงเดียวกันหรือค่าเดียวกันสำหรับดั้งเดิม
jfriend00

12
โดย "ปรับแต่งความเท่าเทียมกัน" ฉันหมายถึงวิธีที่นักพัฒนาสามารถกำหนดวัตถุสองสามอย่างที่จะถือว่าเท่ากันหรือไม่
czerny

นอกจากนี้stackoverflow.com/q/10539938/632951
Pacerier

คำตอบ:


107

SetวัตถุES6 ไม่มีวิธีการเปรียบเทียบหรือความสามารถในการเปรียบเทียบแบบกำหนดเอง

.has(), .add()และ.delete()วิธีการทำงานเฉพาะปิดมันเป็นวัตถุที่เกิดขึ้นจริงเดียวกันหรือค่าเดียวกันสำหรับดั้งเดิมและไม่ได้มีวิธีการที่จะเสียบเข้าหรือเปลี่ยนเพียงแค่ว่าตรรกะ

คุณคงจะสามารถได้มาซึ่งวัตถุของคุณเองจากSetและแทนที่.has(), .add()และ.delete()วิธีการที่มีบางสิ่งบางอย่างที่ไม่ได้เปรียบเทียบวัตถุลึกแรกที่จะพบว่ารายการที่มีอยู่แล้วในชุด แต่ประสิทธิภาพอาจจะไม่ดีตั้งแต่พื้นฐานSetวัตถุจะไม่ได้รับการช่วย เลย .add()คุณอาจจะมีเพียงแค่ทำซ้ำแรงเดรัจฉานผ่านวัตถุที่มีอยู่ทั้งหมดเพื่อหาการแข่งขันโดยใช้ของคุณเองเปรียบเทียบก่อนที่จะเรียกเดิม

นี่คือข้อมูลจากบทความนี้และการอภิปรายเกี่ยวกับคุณสมบัติของ ES6:

5.2 ทำไมฉันไม่สามารถกำหนดค่าวิธีการแมปและตั้งค่าการเปรียบเทียบคีย์และค่าได้

คำถาม: มันจะดีถ้ามีวิธีกำหนดค่าคีย์แผนที่และองค์ประกอบของชุดใดที่ถือว่าเท่ากัน ทำไมถึงไม่มี

คำตอบ: คุณลักษณะนี้ถูกเลื่อนออกไปเนื่องจากเป็นการยากที่จะใช้งานอย่างถูกต้องและมีประสิทธิภาพ ทางเลือกหนึ่งคือการส่งกลับคอลเลกชันที่ระบุความเท่าเทียมกัน

ตัวเลือกอื่นที่มีใน Java คือการระบุความเท่าเทียมกันผ่านวิธีการที่วัตถุนำไปใช้ (equals () ใน Java) อย่างไรก็ตามวิธีการนี้เป็นปัญหาสำหรับวัตถุที่ไม่แน่นอน: โดยทั่วไปหากวัตถุมีการเปลี่ยนแปลง“ ตำแหน่ง” ภายในคอลเลกชันก็ต้องเปลี่ยนเช่นกัน แต่นั่นไม่ใช่สิ่งที่เกิดขึ้นใน Java JavaScript อาจเป็นเส้นทางที่ปลอดภัยกว่าในการเปิดใช้งานการเปรียบเทียบตามค่าสำหรับวัตถุที่ไม่เปลี่ยนรูปแบบพิเศษเท่านั้น (เรียกว่าวัตถุค่า) การเปรียบเทียบตามค่าหมายความว่าสองค่านั้นถือว่าเท่ากันถ้าเนื้อหาเท่ากัน ค่าดั้งเดิมถูกเปรียบเทียบโดยค่าใน 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วิธีการของพวกมันคืนค่าเท่าเดิม


คุณสามารถให้นวกรรมิกใช้ฟังก์ชันที่เปรียบเทียบไอเท็มเพื่อความเท่าเทียมกัน นี่เป็นสิ่งที่ดีถ้าคุณต้องการให้ความเท่าเทียมกันนี้เป็นคุณลักษณะของชุดแทนที่จะเป็นวัตถุที่ใช้
Ben J

1
@BenJ จุดของการสร้างสตริงและวางไว้ในแผนที่ก็คือวิธีที่เครื่องมือ Javascript ของคุณจะใช้การค้นหา ~ O (1) ในโค้ดเนทีฟเพื่อค้นหาค่าแฮชของวัตถุของคุณในขณะที่การยอมรับฟังก์ชั่นความเท่าเทียม เพื่อทำการสแกนเชิงเส้นของชุดและตรวจสอบทุกองค์ประกอบ
Jamby

3
สิ่งหนึ่งที่ท้าทายด้วยวิธีนี้คือฉันคิดว่ามันถือว่าค่าitem.toIdString()คงที่และไม่สามารถเปลี่ยนแปลงได้ เพราะถ้าทำได้สามารถทำให้GeneralSetกลายเป็นไม่ถูกต้องได้ง่ายด้วยรายการ "ซ้ำ" ในนั้น ดังนั้นวิธีแก้ปัญหาเช่นนั้นจะถูก จำกัด เฉพาะบางสถานการณ์ที่เป็นไปได้ที่ตัววัตถุจะไม่เปลี่ยนแปลงในขณะที่ใช้ชุดหรือในกรณีที่ชุดที่ไม่ถูกต้องนั้นจะไม่เกิดขึ้น ปัญหาทั้งหมดเหล่านี้อาจอธิบายเพิ่มเติมได้ว่าทำไมชุด ES6 จึงไม่เปิดเผยฟังก์ชันการทำงานนี้เนื่องจากสามารถใช้งานได้ในบางสถานการณ์เท่านั้น
jfriend00

เป็นไปได้ที่จะเพิ่มการใช้งานที่ถูกต้องของ.delete()คำตอบนี้หรือไม่?
jlewkovich

1
@JLewkovich แน่นอน
czerny

6

ในฐานะที่เป็นคำตอบด้านบนกล่าวถึงการปรับแต่งความเท่าเทียมกันเป็นปัญหาสำหรับวัตถุที่ไม่แน่นอน ข่าวดีก็คือ (และฉันประหลาดใจที่ยังไม่มีใครพูดถึงเรื่องนี้) มีห้องสมุดยอดนิยมที่เรียกว่าimmutable-jsซึ่งมีชุดประเภทที่ไม่เปลี่ยนรูปซึ่งให้ความหมายที่เท่าเทียมกันในคุณค่าที่คุณกำลังมองหา

นี่คือตัวอย่างของคุณโดยใช้ไม่เปลี่ยนรูป -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
ประสิทธิภาพของชุด / แผนที่ไม่เปลี่ยนรูป -js เปรียบเทียบกับชุด / แผนที่ดั้งเดิมอย่างไร
frankster

5

เพื่อเพิ่มคำตอบที่นี่ฉันไปข้างหน้าและนำแผนที่ wrapper ที่ใช้ฟังก์ชั่นแฮชที่กำหนดเอง, ฟังก์ชั่นความเท่าเทียมกันที่กำหนดเองและเก็บค่าที่แตกต่างที่มีแฮชเทียบเท่า (กำหนดเอง) ในถัง

ไม่คาดฝันก็เปิดออกมาจะช้ากว่าวิธีการ concatenation สตริง Czerny ของ

แหล่งข้อมูลเต็มรูปแบบที่นี่: https://github.com/makoConstruct/ValueMap


“ การต่อสตริง” หรือไม่ วิธีการของเขาไม่เหมือน "สตริงตัวแทน" (ถ้าคุณจะให้ชื่อ) หรือมีเหตุผลที่คุณใช้คำว่า "เรียงต่อกัน"? ฉันอยากรู้ ;-)
binki

@binki นี่เป็นคำถามที่ดีและฉันคิดว่าคำตอบนั้นเป็นจุดที่ดีที่ฉันต้องใช้เวลาซักหน่อย โดยทั่วไปเมื่อคำนวณรหัสแฮชจะทำบางสิ่งเช่นHashCodeBuilderซึ่งคูณรหัสแฮชของแต่ละฟิลด์และไม่รับประกันว่าจะไม่ซ้ำกัน (จึงจำเป็นต้องใช้ฟังก์ชันความเท่าเทียมที่กำหนดเอง) อย่างไรก็ตามเมื่อสร้างสตริง id คุณเชื่อมสตริง id ของแต่ละฟิลด์ซึ่งรับประกันว่าไม่ซ้ำกัน (และไม่จำเป็นต้องมีฟังก์ชันความเท่าเทียมกัน)
Pace

ดังนั้นหากคุณมีPointกำหนดให้เป็น{ x: number, y: number }แล้วคุณน่าจะเป็นid string x.toString() + ',' + y.toString()
Pace

การเปรียบเทียบความเท่าเทียมกันของคุณสร้างมูลค่าบางอย่างซึ่งรับประกันว่าจะแตกต่างกันเฉพาะเมื่อสิ่งต่าง ๆ ควรได้รับการพิจารณาว่าไม่เท่าเทียมกันเป็นกลยุทธ์ที่ฉันเคยใช้มาก่อน บางครั้งมันก็ง่ายที่จะคิดเกี่ยวกับสิ่งต่างๆ ในกรณีที่คุณกำลังสร้างคีย์มากกว่าhashes ตราบใดที่คุณมีคีย์สืบทอดมาซึ่งส่งออกคีย์ในรูปแบบที่เครื่องมือที่มีอยู่สนับสนุนด้วยความเท่าเทียมกันในรูปแบบคุณค่าซึ่งเกือบจะเป็นไปได้เสมอ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 แบบกำหนดเองได้ ก่อนอื่นเราเขียนวิธีการ

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

ชุด

ตอนนี้เราใช้ชุด แต่เราใช้ชุดของสตริงแทนวัตถุ

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
อ่าใช่ "แปลงเป็นสตริง" คำตอบของ Javascript สำหรับทุกสิ่ง
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วิธีการ ในกรณีของฉันฉัน stringify 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 = จริง

set1 เท่ากับ set4 = false


2
คำตอบนี้เกี่ยวข้องกับคำถามหรือไม่ คำถามเกี่ยวกับความเท่าเทียมกันของรายการที่เกี่ยวกับอินสแตนซ์ของ Set class คำถามนี้ดูเหมือนว่าจะหารือเกี่ยวกับความเท่าเทียมกันของทั้งสองชุดกรณี
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.