Bắt chước bộ trong JavaScript?


220

Tôi đang làm việc với JavaScript. Tôi muốn lưu trữ một danh sách các giá trị chuỗi duy nhất , không có thứ tự, với các thuộc tính sau:

  1. một cách nhanh chóng để hỏi 'là A trong danh sách'?
  2. một cách nhanh chóng để làm 'xóa A khỏi danh sách nếu nó tồn tại trong danh sách'
  3. một cách nhanh chóng để làm 'thêm A vào danh sách nếu nó chưa xuất hiện'.

Những gì tôi thực sự muốn là một bộ. Bất kỳ đề xuất nào cho cách tốt nhất để bắt chước một bộ trong JavaScript?

Câu hỏi này khuyên bạn nên sử dụng Đối tượng , với các thuộc tính lưu trữ khóa và tất cả các giá trị được đặt thành đúng: đó có phải là cách hợp lý không?



Câu trả lời:


262

Nếu bạn đang lập trình trong môi trường có khả năng ES6 (chẳng hạn như node.js, một trình duyệt cụ thể có khả năng ES6 bạn cần hoặc dịch mã ES6 cho môi trường của bạn), thì bạn có thể sử dụng Setđối tượng được tích hợp trong ES6 . Nó có khả năng rất tốt và có thể được sử dụng như trong môi trường của bạn.


Đối với nhiều thứ đơn giản trong môi trường ES5, sử dụng Object hoạt động rất tốt. Nếu objlà đối tượng của bạn và Alà một biến có giá trị bạn muốn thao tác trong tập hợp, thì bạn có thể thực hiện các thao tác sau:

Mã khởi tạo:

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

Câu 1:Atrong danh sách:

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

Câu hỏi 2: Xóa 'A' khỏi danh sách nếu có:

delete obj[A];

Câu hỏi 3: Thêm 'A' vào danh sách nếu nó chưa có

obj[A] = true;

Để đầy đủ, thử nghiệm cho dù Atrong danh sách có an toàn hơn một chút với điều này:

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

do xung đột tiềm ẩn giữa các phương thức tích hợp và / hoặc các thuộc tính trên Đối tượng cơ sở như thuộc constructortính.


Thanh bên trên ES6: Phiên bản làm việc hiện tại của ECMAScript 6 hoặc đôi khi được gọi là ES 2015 có đối tượng Set tích hợp . Nó được thực hiện ngay bây giờ trong một số trình duyệt. Vì tính khả dụng của trình duyệt thay đổi theo thời gian, bạn có thể nhìn vào dòng Settrong bảng tương thích ES6 này để xem trạng thái hiện tại cho tính khả dụng của trình duyệt.

Một lợi thế của đối tượng Set tích hợp là nó không ép buộc tất cả các khóa vào một chuỗi như Object làm để bạn có thể có cả 5 và "5" làm các khóa riêng biệt. Và, bạn thậm chí có thể sử dụng Đối tượng trực tiếp trong tập hợp mà không cần chuyển đổi chuỗi. Đây là một bài viết mô tả một số khả năng và tài liệu của MDN về đối tượng Set.

Bây giờ tôi đã viết một polyfill cho đối tượng thiết lập ES6 để bạn có thể bắt đầu sử dụng ngay bây giờ và nó sẽ tự động chuyển sang đối tượng thiết lập sẵn nếu trình duyệt hỗ trợ. Điều này có lợi thế là bạn đang viết mã tương thích ES6 sẽ hoạt động hoàn toàn trở lại IE7. Nhưng, có một số nhược điểm. Giao diện thiết lập ES6 tận dụng các trình lặp ES6 để bạn có thể thực hiện những việc như thế for (item of mySet)và nó sẽ tự động lặp lại thông qua bộ cho bạn. Nhưng, loại tính năng ngôn ngữ này không thể được thực hiện thông qua polyfill. Bạn vẫn có thể lặp lại một bộ ES6 mà không cần sử dụng các tính năng ngôn ngữ ES6 mới, nhưng thật lòng mà không có các tính năng ngôn ngữ mới, nó không tiện lợi như giao diện thiết lập khác mà tôi đưa vào bên dưới.

Bạn có thể quyết định cái nào phù hợp nhất với bạn sau khi nhìn vào cả hai. Bộ polyfill ES6 có tại đây: https://github.com/jfriend00/ES6-set .

FYI, trong thử nghiệm của riêng tôi, tôi đã nhận thấy rằng việc triển khai Firefox v29 Set không được cập nhật đầy đủ trên bản nháp hiện tại của thông số kỹ thuật. Ví dụ: bạn không thể thực hiện các .add()cuộc gọi phương thức như thông số kỹ thuật mô tả và hỗ trợ polyfill của tôi. Đây có lẽ là một vấn đề của một đặc điểm kỹ thuật trong chuyển động vì nó chưa được hoàn thiện.


Các đối tượng được thiết lập sẵn: Nếu bạn muốn một đối tượng đã được xây dựng có các phương thức để vận hành trên một tập hợp mà bạn có thể sử dụng trong bất kỳ trình duyệt nào, bạn có thể sử dụng một loạt các đối tượng dựng sẵn khác nhau thực hiện các loại tập hợp khác nhau. Có một miniset là một mã nhỏ thực hiện những điều cơ bản của một đối tượng được thiết lập. Nó cũng có một đối tượng tập hợp nhiều tính năng hơn và một số dẫn xuất bao gồm Từ điển (cho phép bạn lưu trữ / truy xuất giá trị cho mỗi khóa) và Đối tượng (cho phép bạn giữ một tập hợp các đối tượng - đối tượng JS hoặc đối tượng DOM nơi bạn cung cấp hàm tạo một khóa duy nhất cho mỗi một hoặc Objectset sẽ tạo khóa cho bạn).

Đây là bản sao mã cho miniset (mã cập nhật nhất có ở đây trên 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
Điều này giải quyết câu hỏi, nhưng để rõ ràng, việc triển khai này sẽ không hiệu quả đối với các tập hợp bên cạnh số nguyên hoặc chuỗi.
mkirk

3
@mkirk - vâng, mục bạn đang lập chỉ mục trong tập hợp phải có biểu diễn chuỗi có thể là khóa chỉ mục (ví dụ: đó là chuỗi hoặc có phương thức toString () mô tả duy nhất mục đó).
jfriend00

4
Để có được các mục trong danh sách, bạn có thể sử dụng Object.keys(obj).
Blixt

3
@Blixt - Object.keys()cần IE9, FF4, Safari 5, Opera 12 trở lên. Có một polyfill cho các trình duyệt cũ hơn ở đây .
jfriend00

1
Đừng sử dụng obj.hasOwnProperty(prop)để kiểm tra thành viên. Sử dụng Object.prototype.hasOwnProperty.call(obj, prop)thay thế, hoạt động ngay cả khi "bộ" chứa giá trị "hasOwnProperty".
davidchambers

72

Bạn có thể tạo một Object không có thuộc tính như

var set = Object.create(null)

có thể hoạt động như một bộ và loại bỏ nhu cầu sử dụng 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

Đẹp, nhưng không chắc tại sao bạn nói rằng "loại bỏ nhu cầu sử dụng hasOwnProperty"
blueFast

13
Nếu bạn chỉ sử dụng, set = {}nó sẽ kế thừa tất cả các thuộc tính từ Object (ví dụ toString), vì vậy bạn sẽ phải kiểm tra tải trọng của tập hợp (thuộc tính bạn đã thêm) với hasOwnPropertytrongif (A in set)
Thorben Croisé

6
Tôi không biết rằng có thể tạo ra một đối tượng hoàn toàn trống rỗng. Cảm ơn, giải pháp của bạn rất thanh lịch.
blueFast

1
Thú vị, nhưng nhược điểm của điều này chắc chắn là bạn phải có các set[A]=truetuyên bố cho mọi yếu tố bạn muốn thêm thay vì chỉ một trình khởi tạo?
vogomatix

1
Không chắc ý của bạn là gì, nhưng nếu bạn đang đề cập đến việc khởi tạo một bộ bằng một bộ đã có sẵn, bạn có thể làm gì đó theo dòng củas = Object.create(null);s["thorben"] = true;ss = Object.create(s)
Thorben Croisé

23

Kể từ ECMAScript 6, cấu trúc dữ liệu Set là một tính năng tích hợp . Khả năng tương thích với các phiên bản node.js có thể được tìm thấy ở đây .


4
Xin chào, để rõ ràng - bây giờ là năm 2014, đây có còn là thử nghiệm trong Chrome không? Nếu không, bạn có thể chỉnh sửa câu trả lời của mình không? Cảm ơn
Karel Bílek 17/214

1
Có, nó vẫn đang thử nghiệm cho Chrome. Tôi tin rằng vào cuối năm 2014, khi ECMAScript được cho là 'chính thức' được phát hành, nó sẽ được hỗ trợ. Sau đó tôi sẽ cập nhật câu trả lời của tôi cho phù hợp.
thánh ca

OK, cảm ơn vì đã trả lời! (Câu trả lời JavaScript bị lỗi thời khá nhanh.)
Karel Bílek

1
@Val inkhông hoạt động vì Setcác đối tượng không có các phần tử của nó là các thuộc tính, điều này sẽ không tốt vì các tập hợp có thể có các phần tử thuộc bất kỳ loại nào, nhưng các thuộc tính là các chuỗi. Bạn có thể sử dụng has:Set([1,2]).has(1)
Oriol

1
Câu trả lời của Salvador Dali là toàn diện và cập nhật hơn.
Dan Dascalescu

14

Trong ES6 phiên bản của javascript bạn đã xây dựng trong kiểu cho bộ ( tương thích với kiểm tra trình duyệt của bạn ).

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

Để thêm một phần tử vào tập bạn chỉ cần sử dụng .add(), phần này chạy vào O(1)và thêm phần tử cần đặt (nếu nó không tồn tại) hoặc không làm gì nếu nó đã ở đó. Bạn có thể thêm phần tử của bất kỳ loại nào ở đó (mảng, chuỗi, số)

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

Để kiểm tra số lượng phần tử trong tập hợp, bạn chỉ cần sử dụng .size. Cũng chạy trongO(1)

numbers.size; // 4

Để loại bỏ phần tử khỏi tập sử dụng .delete(). Nó trả về true nếu giá trị ở đó (và đã bị xóa) và false nếu giá trị không tồn tại. Cũng chạy vào O(1).

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

Để kiểm tra xem phần tử có tồn tại trong tập sử dụng hay không .has(), nó trả về true nếu phần tử nằm trong tập và ngược lại. Cũng chạy vào O(1).

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

Ngoài các phương pháp bạn muốn, có một vài phương pháp bổ sung:

  • numbers.clear(); sẽ loại bỏ tất cả các yếu tố khỏi bộ
  • numbers.forEach(callback); lặp qua các giá trị của tập hợp theo thứ tự chèn
  • numbers.entries(); tạo một vòng lặp của tất cả các giá trị
  • numbers.keys(); trả về các khóa của tập hợp giống như numbers.values()

Ngoài ra còn có Weakset cho phép chỉ thêm các giá trị loại đối tượng.


bạn có thể chỉ một tham chiếu để .add()chạy trong O (1) không? Tôi bị thu hút bởi điều này,
Green

10

Tôi đã bắt đầu triển khai Bộ hiện đang hoạt động khá tốt với số và chuỗi. Trọng tâm chính của tôi là hoạt động khác biệt, vì vậy tôi đã cố gắng làm cho nó hiệu quả nhất có thể. Dĩa và mã đánh giá được chào đón!

https://github.com/mcrisc/setJS


wow lớp học này là hạt dẻ! Tôi hoàn toàn sẽ sử dụng điều này nếu tôi không viết JavaScript bên trong các hàm giảm / ánh xạ CouchDB!
portforwardpodcast

9

Tôi chỉ nhận thấy rằng thư viện d3.js có triển khai các bộ, bản đồ và các cấu trúc dữ liệu khác. Tôi không thể tranh luận về hiệu quả của chúng nhưng đánh giá bằng thực tế rằng đó là một thư viện phổ biến, nó phải là thứ bạn cần.

Tài liệu ở đây

Để thuận tiện, tôi sao chép từ liên kết (3 chức năng đầu tiên là những chức năng đáng quan tâm)


  • d3.set ([mảng])

Xây dựng một bộ mới. Nếu mảng được chỉ định, thêm mảng giá trị chuỗi đã cho vào tập trả về.

  • set.has (giá trị)

Trả về true khi và chỉ khi tập hợp này có một mục nhập cho chuỗi giá trị được chỉ định.

  • set.add (giá trị)

Thêm chuỗi giá trị được chỉ định vào bộ này.

  • set.remove (giá trị)

Nếu tập hợp chứa chuỗi giá trị được chỉ định, hãy xóa nó và trả về true. Mặt khác, phương thức này không làm gì và trả về false.

  • set.values ​​()

Trả về một mảng các giá trị chuỗi trong tập hợp này. Thứ tự của các giá trị được trả về là tùy ý. Có thể được sử dụng như một cách thuận tiện để tính toán các giá trị duy nhất cho một chuỗi các chuỗi. Ví dụ:

d3.set (["foo", "bar", "foo", "baz"]). value (); // "foo", "thanh", "baz"

  • set.forEach (chức năng)

Gọi hàm được chỉ định cho từng giá trị trong tập hợp này, truyền giá trị dưới dạng đối số. Bối cảnh của chức năng là bộ này. Trả về không xác định. Thứ tự lặp là tùy ý.

  • set.empty ()

Trả về true khi và chỉ khi tập hợp này có giá trị 0.

  • set.size ()

Trả về số lượng giá trị trong bộ này.


4

Vâng, đó là một cách hợp lý - đó là tất cả một đối tượng (tốt, đối với trường hợp sử dụng này) - một loạt các khóa / giá trị với quyền truy cập trực tiếp.

Bạn cần kiểm tra xem nó đã ở đó chưa trước khi thêm nó, hoặc nếu bạn chỉ cần chỉ ra sự hiện diện, "thêm" nó một lần nữa không thực sự thay đổi bất cứ điều gì, nó chỉ đặt lại vào đối tượng.

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.