การเลียนแบบชุดใน JavaScript?


220

ฉันทำงานกับ JavaScript ฉันต้องการเก็บรายการของค่าสตริงที่ไม่ซ้ำกันและไม่เรียงลำดับด้วยคุณสมบัติดังต่อไปนี้:

  1. วิธีที่รวดเร็วในการถาม 'เป็น A ในรายการ' หรือไม่
  2. วิธีที่รวดเร็วในการทำ 'ลบ A ออกจากรายการหากมีอยู่ในรายการ'
  3. วิธีที่รวดเร็วในการทำ 'เพิ่ม A ลงในรายการหากยังไม่ปรากฏ'

สิ่งที่ฉันต้องการคือชุด คำแนะนำใด ๆ สำหรับวิธีที่ดีที่สุดในการเลียนแบบชุดใน JavaScript?

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



คำตอบ:


262

หากคุณกำลังเขียนโปรแกรมในสภาพแวดล้อมที่มีความสามารถ ES6 (เช่น Node.js, เบราว์เซอร์ที่เฉพาะเจาะจงที่มีความสามารถ ES6 ที่คุณต้องการหรือ transpiling รหัส ES6 สำหรับสภาพแวดล้อมของคุณ) แล้วคุณสามารถใช้Setวัตถุที่สร้างขึ้นใน ES6 มันมีความสามารถที่ดีมากและสามารถใช้งานได้ตามที่คุณต้องการ


สำหรับสิ่งง่าย ๆ มากมายในสภาพแวดล้อม ES5 การใช้ Object ทำงานได้ดีมาก หากobjเป็นวัตถุของคุณและAเป็นตัวแปรที่มีค่าที่คุณต้องการดำเนินการในชุดคุณสามารถทำสิ่งเหล่านี้ได้:

รหัสการเริ่มต้น:

// create empty object
var obj = {};

// or create an object with some items already in it
var obj = {"1":true, "2":true, "3":true, "9":true};

คำถามที่ 1:อยู่Aในรายการ:

if (A in obj) {
    // put code here
}

คำถามที่ 2:ลบ 'A' ออกจากรายการหากมี:

delete obj[A];

คำถามที่ 3:เพิ่ม 'A' ลงในรายการหากยังไม่มี

obj[A] = true;

เพื่อความสมบูรณ์การทดสอบว่าAอยู่ในรายการหรือไม่นั้นปลอดภัยกว่าด้วย:

if (Object.prototype.hasOwnProperty.call(obj, A))
    // put code here
}

เนื่องจากความขัดแย้งที่อาจเกิดขึ้นระหว่างวิธีการในตัวและ / หรือคุณสมบัติบนวัตถุฐานเช่นconstructorคุณสมบัติ


แถบด้านข้างของ ES6: ECMAScript 6รุ่นที่ใช้งานได้ในปัจจุบันหรือบางสิ่งที่เรียกว่า ES 2015 มีชุดวัตถุในตัว มันมีการใช้งานแล้วในเบราว์เซอร์บางตัว เนื่องจากความพร้อมใช้งานของเบราว์เซอร์เปลี่ยนแปลงตลอดเวลาคุณสามารถดูบรรทัดSetในตารางความเข้ากันได้ ES6 นี้เพื่อดูสถานะปัจจุบันของความพร้อมใช้งานของเบราว์เซอร์

ข้อดีอย่างหนึ่งของการตั้งค่าวัตถุในตัวคือมันไม่ได้บังคับคีย์ทั้งหมดให้เป็นสตริงเหมือนกับที่วัตถุทำเพื่อให้คุณสามารถมีทั้ง 5 และ "5" เป็นคีย์แยกต่างหาก และคุณยังสามารถใช้วัตถุได้โดยตรงในชุดโดยไม่ต้องแปลงสตริง นี่คือบทความที่อธิบายถึงความสามารถบางอย่างและเอกสารประกอบของ MDNในชุดวัตถุ

ตอนนี้ฉันได้เขียนโปลิฟิลสำหรับชุดวัตถุ ES6 เพื่อให้คุณสามารถเริ่มใช้งานได้ในตอนนี้และมันจะเลื่อนไปที่ชุดวัตถุที่ติดตั้งไว้ภายในโดยอัตโนมัติหากเบราว์เซอร์รองรับ นี่เป็นข้อได้เปรียบที่คุณกำลังเขียนโค้ดที่เข้ากันได้กับ ES6 ซึ่งจะทำงานกลับไปสู่ ​​IE7 แต่มีข้อเสียอยู่บ้าง อินเทอร์เฟซการตั้งค่า ES6 ใช้ประโยชน์จากตัววนซ้ำ ES6 เพื่อให้คุณสามารถทำสิ่งต่าง ๆ ได้for (item of mySet)และมันจะวนซ้ำอัตโนมัติผ่านการตั้งค่าสำหรับคุณ แต่คุณสมบัติภาษาประเภทนี้ไม่สามารถใช้งานผ่านทาง polyfill คุณยังสามารถย้ำชุด ES6 ได้โดยไม่ต้องใช้คุณสมบัติภาษา ES6 ใหม่ แต่ตรงไปตรงมาหากไม่มีคุณลักษณะภาษาใหม่มันไม่สะดวกเท่าอินเทอร์เฟซชุดอื่นที่ฉันรวมไว้ด้านล่าง

คุณสามารถตัดสินใจได้ว่าอันไหนดีที่สุดสำหรับคุณหลังจากดูทั้งสองอย่าง ES6 ชุด polyfill อยู่ที่นี่: https://github.com/jfriend00/ES6-Set

FYI ในการทดสอบของฉันเองฉันพบว่าการติดตั้ง Firefox v29 นั้นไม่ได้เป็นข้อมูลที่สมบูรณ์เกี่ยวกับร่างปัจจุบันของข้อมูลจำเพาะ ตัวอย่างเช่นคุณไม่สามารถเชื่อมโยง.add()วิธีการแบบเช่นข้อมูลจำเพาะอธิบายและโพลีฟิลของฉันรองรับ นี่อาจเป็นเรื่องของข้อมูลจำเพาะในการเคลื่อนไหวตามที่ยังไม่ได้สรุป


วัตถุชุดที่สร้างไว้ล่วงหน้า:หากคุณต้องการวัตถุที่สร้างขึ้นแล้วซึ่งมีวิธีการปฏิบัติการในชุดที่คุณสามารถใช้ในเบราว์เซอร์ใด ๆ คุณสามารถใช้ชุดของวัตถุที่สร้างไว้ล่วงหน้าที่แตกต่างกันซึ่งใช้ชุดประเภทต่างๆ มี miniSet ซึ่งเป็นรหัสขนาดเล็กที่ใช้พื้นฐานของชุดวัตถุ นอกจากนี้ยังมีชุดวัตถุที่มีคุณสมบัติมากขึ้นและการสืบทอดหลายอย่างรวมถึงพจนานุกรม (ให้คุณจัดเก็บ / ดึงค่าสำหรับแต่ละคีย์) และชุดวัตถุ (ให้คุณเก็บชุดของวัตถุ - วัตถุ JS หรือวัตถุ DOM ที่คุณจัดหา ฟังก์ชันที่สร้างคีย์เฉพาะสำหรับแต่ละอันหรือ ObjectSet จะสร้างคีย์ให้คุณ)

นี่คือสำเนาของรหัสสำหรับ miniSet (รหัสที่ทันสมัยที่สุดอยู่ที่นี่ใน github )

"use strict";
//-------------------------------------------
// Simple implementation of a Set in javascript
//
// Supports any element type that can uniquely be identified
//    with its string conversion (e.g. toString() operator).
// This includes strings, numbers, dates, etc...
// It does not include objects or arrays though
//    one could implement a toString() operator
//    on an object that would uniquely identify
//    the object.
// 
// Uses a javascript object to hold the Set
//
// This is a subset of the Set object designed to be smaller and faster, but
// not as extensible.  This implementation should not be mixed with the Set object
// as in don't pass a miniSet to a Set constructor or vice versa.  Both can exist and be
// used separately in the same project, though if you want the features of the other
// sets, then you should probably just include them and not include miniSet as it's
// really designed for someone who just wants the smallest amount of code to get
// a Set interface.
//
// s.add(key)                      // adds a key to the Set (if it doesn't already exist)
// s.add(key1, key2, key3)         // adds multiple keys
// s.add([key1, key2, key3])       // adds multiple keys
// s.add(otherSet)                 // adds another Set to this Set
// s.add(arrayLikeObject)          // adds anything that a subclass returns true on _isPseudoArray()
// s.remove(key)                   // removes a key from the Set
// s.remove(["a", "b"]);           // removes all keys in the passed in array
// s.remove("a", "b", ["first", "second"]);   // removes all keys specified
// s.has(key)                      // returns true/false if key exists in the Set
// s.isEmpty()                     // returns true/false for whether Set is empty
// s.keys()                        // returns an array of keys in the Set
// s.clear()                       // clears all data from the Set
// s.each(fn)                      // iterate over all items in the Set (return this for method chaining)
//
// All methods return the object for use in chaining except when the point
// of the method is to return a specific value (such as .keys() or .isEmpty())
//-------------------------------------------


// polyfill for Array.isArray
if(!Array.isArray) {
    Array.isArray = function (vArg) {
        return Object.prototype.toString.call(vArg) === "[object Array]";
    };
}

function MiniSet(initialData) {
    // Usage:
    // new MiniSet()
    // new MiniSet(1,2,3,4,5)
    // new MiniSet(["1", "2", "3", "4", "5"])
    // new MiniSet(otherSet)
    // new MiniSet(otherSet1, otherSet2, ...)
    this.data = {};
    this.add.apply(this, arguments);
}

MiniSet.prototype = {
    // usage:
    // add(key)
    // add([key1, key2, key3])
    // add(otherSet)
    // add(key1, [key2, key3, key4], otherSet)
    // add supports the EXACT same arguments as the constructor
    add: function() {
        var key;
        for (var i = 0; i < arguments.length; i++) {
            key = arguments[i];
            if (Array.isArray(key)) {
                for (var j = 0; j < key.length; j++) {
                    this.data[key[j]] = key[j];
                }
            } else if (key instanceof MiniSet) {
                var self = this;
                key.each(function(val, key) {
                    self.data[key] = val;
                });
            } else {
                // just a key, so add it
                this.data[key] = key;
            }
        }
        return this;
    },
    // private: to remove a single item
    // does not have all the argument flexibility that remove does
    _removeItem: function(key) {
        delete this.data[key];
    },
    // usage:
    // remove(key)
    // remove(key1, key2, key3)
    // remove([key1, key2, key3])
    remove: function(key) {
        // can be one or more args
        // each arg can be a string key or an array of string keys
        var item;
        for (var j = 0; j < arguments.length; j++) {
            item = arguments[j];
            if (Array.isArray(item)) {
                // must be an array of keys
                for (var i = 0; i < item.length; i++) {
                    this._removeItem(item[i]);
                }
            } else {
                this._removeItem(item);
            }
        }
        return this;
    },
    // returns true/false on whether the key exists
    has: function(key) {
        return Object.prototype.hasOwnProperty.call(this.data, key);
    },
    // tells you if the Set is empty or not
    isEmpty: function() {
        for (var key in this.data) {
            if (this.has(key)) {
                return false;
            }
        }
        return true;
    },
    // returns an array of all keys in the Set
    // returns the original key (not the string converted form)
    keys: function() {
        var results = [];
        this.each(function(data) {
            results.push(data);
        });
        return results;
    },
    // clears the Set
    clear: function() {
        this.data = {}; 
        return this;
    },
    // iterate over all elements in the Set until callback returns false
    // myCallback(key) is the callback form
    // If the callback returns false, then the iteration is stopped
    // returns the Set to allow method chaining
    each: function(fn) {
        this.eachReturn(fn);
        return this;
    },
    // iterate all elements until callback returns false
    // myCallback(key) is the callback form
    // returns false if iteration was stopped
    // returns true if iteration completed
    eachReturn: function(fn) {
        for (var key in this.data) {
            if (this.has(key)) {
                if (fn.call(this, this.data[key], key) === false) {
                    return false;
                }
            }
        }
        return true;
    }
};

MiniSet.prototype.constructor = MiniSet;

16
วิธีนี้จะช่วยแก้ปัญหา แต่เพื่อความชัดเจนการใช้งานนี้จะไม่สามารถใช้ได้กับชุดของสิ่งอื่นนอกเหนือจากจำนวนเต็มหรือสตริง
mkirk

3
@mkirk - ใช่รายการที่คุณกำลังจัดทำดัชนีในชุดจะต้องมีการแสดงสตริงที่สามารถเป็นคีย์ดัชนี (เช่นมันอาจเป็นสตริงหรือมีวิธี toString () ที่อธิบายรายการโดยไม่ซ้ำกัน)
jfriend00

4
Object.keys(obj)จะได้รับรายการในรายการคุณสามารถใช้
Blixt

3
@ Blixt - Object.keys()ต้องการ IE9, FF4, Safari 5, Opera 12 หรือสูงกว่า มี polyfill เบราว์เซอร์รุ่นเก่าเป็นที่นี่
jfriend00

1
อย่าใช้obj.hasOwnProperty(prop)สำหรับการตรวจสอบสมาชิก ใช้Object.prototype.hasOwnProperty.call(obj, prop)แทนซึ่งจะใช้ได้แม้ว่า "set" จะมีค่า"hasOwnProperty"อยู่
davidchambers

72

คุณสามารถสร้างวัตถุที่ไม่มีคุณสมบัติเหมือน

var set = Object.create(null)

hasOwnPropertyซึ่งสามารถทำหน้าที่เป็นชุดและไม่จำเป็นต้องใช้


var set = Object.create(null); // create an object with no properties

if (A in set) { // 1. is A in the list
  // some code
}
delete set[a]; // 2. delete A from the list if it exists in the list 
set[A] = true; // 3. add A to the list if it is not already present

ดี แต่ไม่แน่ใจว่าทำไมคุณถึงพูดว่า "กำจัดความต้องการที่จะใช้ hasOwnProperty"
blueFast

13
ถ้าคุณใช้set = {}มันจะสืบทอดคุณสมบัติทั้งหมดจาก Object (เช่นtoString) ดังนั้นคุณจะต้องตรวจสอบ payload ของชุด (คุณสมบัติที่คุณเพิ่ม) ด้วยhasOwnPropertyในif (A in set)
Thorben Croisé

6
ฉันไม่ทราบว่าเป็นไปได้ที่จะสร้างวัตถุที่ว่างเปล่าอย่างสมบูรณ์ ขอบคุณโซลูชันของคุณงดงามมาก
blueFast

1
ที่น่าสนใจ แต่ข้อเสียของเรื่องนี้คือคุณต้องมีset[A]=trueงบสำหรับทุกองค์ประกอบที่คุณต้องการเพิ่มแทนที่จะเป็นผู้เริ่มต้นเพียงคนเดียวหรือไม่
vogomatix

1
ไม่แน่ใจว่าสิ่งที่คุณหมายถึง แต่ถ้าคุณหมายถึงการเริ่มต้นชุดโดยชุดปัจจุบันที่มีอยู่แล้วคุณสามารถทำบางสิ่งตามแนวของs = Object.create(null);s["thorben"] = true;ss = Object.create(s)
Thorben Croisé

23

ตั้งแต่ ECMAScript 6 ชุดโครงสร้างข้อมูลเป็นคุณสมบัติในตัว ความเข้ากันได้กับรุ่น Node.js สามารถพบได้ที่นี่


4
สวัสดีครับเพื่อความชัดเจน - ปี 2014 ตอนนี้ยังอยู่ในช่วงทดลองหรือไม่? ถ้าไม่ใช่คุณช่วยแก้ไขคำตอบของคุณได้ไหม? ขอบคุณ
Karel Bílek

1
ใช่มันยังอยู่ในช่วงทดลองสำหรับ Chrome ฉันเชื่อว่าภายในสิ้นปี 2014 เมื่อ ECMAScript คาดว่าจะวางจำหน่ายอย่างเป็นทางการจะได้รับการสนับสนุน ฉันจะอัปเดตคำตอบของฉันตามนั้น
Hymloth

ตกลงขอบคุณที่ตอบ! (คำตอบ JavaScript จะล้าสมัยอย่างรวดเร็ว)
Karel Bílek

1
@Val inไม่ทำงานเนื่องจากSetวัตถุไม่มีองค์ประกอบเป็นคุณสมบัติซึ่งจะไม่ดีเนื่องจากชุดสามารถมีองค์ประกอบประเภทใดก็ได้ แต่คุณสมบัติเป็นสตริง คุณสามารถใช้has:Set([1,2]).has(1)
Oriol

1
คำตอบของ Salvador Daliนั้นครอบคลุมและทันสมัยกว่ามากขึ้น
Dan Dascalescu

14

ใน Javascript เวอร์ชัน ES6 คุณได้สร้างชุดไว้แล้ว ( ตรวจสอบความเข้ากันได้กับเบราเซอร์ของคุณ )

var numbers = new Set([1, 2, 4]); // Set {1, 2, 4}

ในการเพิ่มองค์ประกอบให้กับชุดที่คุณใช้.add()ซึ่งจะทำงานO(1)และเพิ่มองค์ประกอบที่จะตั้งค่า (หากไม่มีอยู่) หรือไม่ทำอะไรเลยหากมีอยู่แล้ว คุณสามารถเพิ่มองค์ประกอบประเภทใดก็ได้ (อาร์เรย์, สตริง, ตัวเลข)

numbers.add(4); // Set {1, 2, 4}
numbers.add(6); // Set {1, 2, 4, 6}

เพื่อตรวจสอบจำนวนขององค์ประกอบ.sizeในชุดที่คุณก็สามารถใช้ ยังวิ่งเข้ามาO(1)

numbers.size; // 4

การลบองค์ประกอบจากชุด.delete()การใช้งาน มันจะส่งกลับจริงถ้าค่าอยู่ที่นั่น (และถูกลบ) และเท็จถ้าไม่มีค่า ยังวิ่งเข้าO(1)มา

numbers.delete(2); // true
numbers.delete(2); // false

เพื่อตรวจสอบว่าองค์ประกอบมีอยู่ในชุดการใช้งาน.has()ซึ่งจะส่งกลับจริงถ้าองค์ประกอบอยู่ในชุดและเท็จอย่างอื่น ยังวิ่งเข้าO(1)มา

numbers.has(3); // false
numbers.has(1); // true

นอกเหนือจากวิธีการที่คุณต้องการยังมีอีกสองสามวิธี:

  • numbers.clear(); จะลบองค์ประกอบทั้งหมดออกจากชุด
  • numbers.forEach(callback); วนซ้ำผ่านค่าของชุดในลำดับการแทรก
  • numbers.entries(); สร้างตัววนซ้ำของค่าทั้งหมด
  • numbers.keys(); ส่งคืนคีย์ของชุดซึ่งเหมือนกับ numbers.values()

นอกจากนี้ยังมี Weakset ซึ่งอนุญาตให้เพิ่มเฉพาะค่าประเภทวัตถุ


คุณสามารถชี้การอ้างอิงถึงการ.add()ทำงานใน O (1) ได้ไหม ฉันรู้สึกทึ่งกับสิ่งนี้
กรีน

10

ฉันเริ่มใช้ชุดที่ปัจจุบันใช้งานได้ดีกับตัวเลขและสตริง จุดสนใจหลักของฉันคือการทำงานที่แตกต่างดังนั้นฉันจึงพยายามทำให้มีประสิทธิภาพเท่าที่จะทำได้ ขอแสดงความยินดีกับการตรวจสอบรหัสและส้อม!

https://github.com/mcrisc/SetJS


ว้าวคลาสนี้คือถั่ว! ฉันจะใช้สิ่งนี้โดยสิ้นเชิงถ้าฉันไม่ได้เขียน JavaScript ในแผนที่ CouchDB / ลดฟังก์ชั่น!
portforwardpodcast

9

ฉันเพิ่งสังเกตเห็นว่าห้องสมุด d3.js มีการใช้ชุดแผนที่และโครงสร้างข้อมูลอื่น ๆ ฉันไม่สามารถโต้เถียงเกี่ยวกับประสิทธิภาพของพวกเขา แต่ตัดสินโดยความจริงที่ว่ามันเป็นห้องสมุดยอดนิยมมันต้องเป็นสิ่งที่คุณต้องการ

เอกสารประกอบคือ ที่นี่

เพื่อความสะดวกฉันคัดลอกจากลิงค์ (3 ฟังก์ชั่นแรกคือฟังก์ชั่นที่น่าสนใจ)


  • d3.set ([อาร์เรย์])

สร้างชุดใหม่ หากระบุอาเรย์ให้เพิ่มอาเรย์ของค่าสตริงในชุดที่ส่งคืน

  • set.has (ค่า)

ผลตอบแทนจริงถ้าหากชุดนี้มีรายการสำหรับสตริงค่าที่ระบุ

  • set.add (ค่า)

เพิ่มสตริงค่าที่ระบุให้กับชุดนี้

  • set.remove (ค่า)

หากชุดมีสตริงค่าที่ระบุเอามันออกและส่งกลับจริง มิฉะนั้นวิธีนี้จะไม่ทำอะไรเลยและคืนค่าเท็จ

  • set.values ​​()

ส่งคืนอาร์เรย์ของค่าสตริงในชุดนี้ ลำดับของค่าที่ส่งคืนนั้นเป็นกฎเกณฑ์ สามารถใช้เป็นวิธีที่สะดวกในการคำนวณค่าที่ไม่ซ้ำสำหรับชุดสตริง ตัวอย่างเช่น:

d3.set (["foo", "bar", "foo", "baz"]). values ​​(); // "foo", "bar", "baz"

  • set.forEach (ฟังก์ชั่น)

เรียกใช้ฟังก์ชันที่ระบุสำหรับแต่ละค่าในชุดนี้ส่งผ่านค่าเป็นอาร์กิวเมนต์ บริบทของฟังก์ชันนี้คือชุดนี้ ส่งคืนไม่ได้กำหนด คำสั่งการทำซ้ำเป็นกฎเกณฑ์

  • set.empty ()

ผลตอบแทนจริงถ้าหากชุดนี้มีค่าเป็นศูนย์

  • set.size ()

ส่งคืนจำนวนค่าในชุดนี้


4

ใช่นั่นเป็นวิธีที่สมเหตุสมผล - นั่นคือทั้งหมดที่วัตถุ (ดีสำหรับกรณีการใช้งานนี้) - พวงของคีย์ / ค่าที่มีการเข้าถึงโดยตรง

คุณจะต้องตรวจสอบเพื่อดูว่ามันมีอยู่แล้วก่อนที่จะเพิ่มหรือถ้าคุณเพียงแค่ต้องระบุสถานะ "เพิ่ม" อีกครั้งไม่ได้เปลี่ยนแปลงอะไรจริง ๆ มันแค่วางมันบนวัตถุอีกครั้ง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.