JavaScript ES6 Set and WeakSet Object 物件

ES6 新增了 Set 和 WeakSet 數據結構。

Set

Set 有點像是陣列 (array),Set 中的元素 (element) 可以是任何資料型態,兩者不同的地方在於 Set 中所有的值都是唯一的 (unique values),不會有重複的值,當你存入重複值 (duplicate values) 會被忽略。

當你向 Set 中加入新元素時,Set 內部會用 === 來判斷是否有重複值,唯一的例外是 NaN 會被判斷作是重複的值,雖然 NaN !== NaN

而 Set 中的資料也是有序的,當你遍歷一個 Set 資料結構時,會依照先前寫入的順序 (insertion order)。

建立 Set 物件的語法:

// 建立一個空 Set
new Set()

// 建立一個 Set 並初始化
new Set(iterable)

使用範例:

var mySet = new Set();

// add() 用來新增元素

mySet.add(1);
mySet.add(5);
mySet.add('some text');
var o = {a: 1, b: 2};
mySet.add(o);

// 因為 o !== {a: 1, b: 2} 所以可以加入
mySet.add({a: 1, b: 2});

// has() 用來判斷 Set 中有沒某個值

// true
mySet.has(1);
// false
mySet.has(3);
// true
mySet.has(5);
// true
mySet.has(Math.sqrt(25));
// true
mySet.has('Some Text'.toLowerCase());
// true
mySet.has(o);

Set 的構造函數 (constructor) 可以傳入一個陣列當作初始化參數:

var mySet = new Set(['value1', 'value2', 'value3']);

// true
mySet.has('value1');

// Set {"value1", "value2", "value3"}
mySet;

Set 物件的屬性和方法 Properties and Methods

Set.prototype.size

用來取得 Set 物件的大小,總共有幾個元素。

var mySet = new Set();

mySet.add(1);
mySet.add(5);
mySet.add('some text')

// 3
mySet.size;

Set.prototype.add(value)

用來新增元素,add() 方法會返回 Set 本身。

var mySet = new Set();

mySet.add(1);
mySet.add(5)
mySet.add('some text');

// Set {1, 5, "some text"}
mySet;

因為 add() 方法會返回 Set 本身,所以你可以用 chaining 的寫法:

var mySet = new Set();

mySet.add(1)
     .add(5)
     .add('some text');

// Set {1, 5, "some text"}
mySet;

Set.prototype.has(value)

返回一個 boolean,判斷 Set 中有沒某個值。

var mySet = new Set();
mySet.add('foo');

// true
mySet.has('foo');
// false
mySet.has('bar');

Set.prototype.delete(value)

刪除某個值,如果刪除成功會返回 true,反之返回 false。

var mySet = new Set();
mySet.add('foo');

// false
mySet.delete('bar');
// true
mySet.delete('foo');

// false
mySet.has('foo');

Set.prototype.clear()

用來清空 Set,刪除所有的元素,沒有返回值。

var mySet = new Set();
mySet.add(1);
mySet.add('foo');

// 2
mySet.size;
// true
mySet.has('foo');

mySet.clear();

// 0
mySet.size;
// false
mySet.has('bar');

Set.prototype.keys() Set.prototype.values()

keys() 和 values() 行為是一樣的,返回一個 Iterator 物件表示所有的元素值。

Set 資料結構中沒有 key,只有 value,或說在 Set 中 key 等於 value。

var mySet = new Set([50, 'a', {foo: 'bar'}]);

for (let v of mySet.values()) {
    console.log(v);
}

// 依序輸出 50 a {foo: "bar"}

for (let k of mySet.keys()) {
    console.log(k);
}

Set.prototype.entries()

返回一個 Iterator 物件,其中每一個值是 [value, value] 結構的陣列。

var mySet = new Set();
mySet.add('foobar');
mySet.add(1);
mySet.add('baz');

for (let item of mySet.entries()) {
    console.log(item);
}

// 依序輸出
// ["foobar", "foobar"]
// [1, 1]
// ["baz", "baz"]

Set.prototype.forEach(callbackFn[, thisArg])

forEach() 方法可以用來遍歷 Set。

其中第一個參數 callbackFn 是一個函數,有三個參數:

  1. value
  2. value
  3. Set 物件本身

為什麼要有兩個同樣的 value?是為了和 MapArray 的 forEach 用法一致。

第二個參數 thisArg 不是必要的參數,表示 this 指向的物件。

function logSetElements(value1, value2, set) {
    console.log('s[' + value1 + '] = ' + value2);
}

new Set(['foo', 'bar', undefined]).forEach(logSetElements);

// 依序輸出
// "s[foo] = foo"
// "s[bar] = bar"
// "s[undefined] = undefined"

for...of

也可以用 for...of 來遍歷 Set。

var mySet = new Set([50, 'a', {foo: 'bar'}]);

for (let v of mySet) {
    console.log(v);
}

// 依序輸出 50 a {foo: "bar"}

WeakSet

WeakSet 和 Set 資料結構基本上是類似的,唯一的區別是 WeakSet 只接受 object 當作元素值 (除了 null 以外,值也不行是 null,會被當作是 undefined 看待),基本資料型態 (primitive data types) 都不能被當作是值。

WeakSet 中的 object 不會被垃圾回收機制 (garbage collection) 計入參考,這也就是 weak 的意思 - 弱引用 (weakly reference)。

因為 weakly reference,所以 WeakSet 中的 object 可能隨時會被自動回收 (garbage collected),而當 object 被回收後,其所對應的元素也會自動被刪除,所以說用 WeakSet 可以方便地避免內存泄露 (memory leak) 的問題。

var ws = new WeakSet();

// 錯誤
// TypeError: Invalid value used in weak set
ws.add(100);

因為 WeakSet weakly reference 的特性,WeakSet 不支援遍歷類型的操作,只支援三個方法 add(), has(), delete()。

WeakSet 的操作基本上和 Set 相似:

var ws = new WeakSet();
var obj = {};
var foo = {};

ws.add(window);
ws.add(obj);

// true
ws.has(window);
// false
ws.has(foo);

// 從 WeakSet 中刪除 window
ws.delete(window);

// false
ws.has(window);

再舉一個 WeakSet 實用的例子:

const foos = new WeakSet();

class Foo {
    constructor() {
        foos.add(this)
    }
    
    method () {
        if (!foos.has(this)) {
            throw new TypeError('Foo.prototype.method 只能在 Foo 物件上被調用');
        }
    }
}

上面的程式碼確保 Foo 的方法,只能被 Foo 物件本身執行,用 WeakSet 你就可以在刪除 Foo 物件時,不用管 foos 中的引用,也不會出現 memory leak。