Set和Map數據結構

文章編寫參考 阮一峯《ECMAScript 6 入門》


1. Set

1.1 基本用法

Set是ES6提供的新的數據結構。它類似於數組,但是成員的值都是唯一的,沒有重複的值。

Set數據結構和Symbol不一樣,Set需要使用構造函數來生成數據結構實例。

const s = new Set();

上面代碼通過Set構造函數生成了一個Set數據實例。

const s = new Set();

[1, 2, 3, 4, 5, 6, 5, 4].forEach(v => s.add(v));

console.log(s);
//  Set { 1, 2, 3, 4, 5, 6 }

上面代碼中通過add方法向Set結構中加入成員,可以看出Set結構中不會添加重複的成員。

【Set允許初始化時傳入一個數組作爲參數(或者具有 iterable 接口的其他數據結構)】,則上面的例子可以改寫爲下面這個樣子。

const s = new Set([1, 2, 3, 4, 5, 6, 5, 4]);

console.log(s);
//Set { 1, 2, 3, 4, 5, 6 }

上面代碼應用數組做爲參數,將數組數據結構轉換成了Set數據結構,並去除了重複數據。
結合擴展運算符,我們便又擁有了一種數組數據去重的方式

const s = new Set([1, 2, 3, 4, 5, 6, 5, 4]);

console.log([...s]);
//[ 1, 2, 3, 4, 5, 6 ]

上面代碼中將原數組中的重複值很輕鬆的就去掉了。

【注意】在向Set中添加元素的時候,不會發生類型的轉換!也就是說字符串‘5’和數值5是兩個數據,不會被去重。而且Set數據結構的內部是使用的嚴格相等運算符(===),但是在Set數據結構中NaN是等於NaN的。

const s = new Set([5, '5']);

console.log(s);
//// Set { 5, '5' }

const s = new Set([NaN,NaN]);
console.log(s);
//Set { NaN }

上面代碼中證明了往Set中插入值得時候不會發生類型的轉換,並且在Set內部,NaN===NaN。

【Set數據結構中,兩個對象總是認爲不相等的】

const s = new Set();

s.add({});
console.log(s.size);    // 1

s.add({});
console.log(s.size);    // 2

上面代碼中Set數據結構中擁有一個空對象之後還能繼續添加一個空對象,證明在Set數據內部,兩個對象是不想等的。

1.2 Set實例的屬性和方法

1.2.1 Set實例屬性
Set實例有以下兩種屬性

  1. Set.prototype.constructor:構造函數,默認就是Set函數。
  2. Set.prototype.size:返回Set實例的成員總數。

1.2.2 Set實例的方法
Set實例方法有【操作方法】和【遍歷方法】兩大類。

1.2.2.1 操作方法

  1. add(value):添加某個值,返回Set結構本身
  2. delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功。
  3. has(value):返回一個布爾值,表示該值是否爲Set的成員。
  4. clear():清除所有成員,沒有返回值。
const s = new Set();

s.add('Blue').add('Crazy');

s.size();   // 2

s.delete('Crazy');  // true

s.has('Crazy'); // false

s.clear();

s.size();   // 0 

上面一組代碼對實例屬性和4個操作方法做出了演示。

【Array.from方法可以將Set數據結構轉換成數組】

const s = new Set([1, 2, 3, 4, 5])
Array.from(s);
//[ 1, 2, 3, 4, 5 ]

// 結合上文中的另外一個方法(利用擴展運算符)

const s = new Set([1, 2, 3, 4, 5])
let arr = [...s]
console.log(arr);
//[ 1, 2, 3, 4, 5 ]

1.2.2.2 遍歷方法

  1. keys():返回鍵名的遍歷器
  2. values():返回鍵值的遍歷器
  3. entries():返回鍵值對的遍歷器
  4. forEach():使用回調函數遍歷每個成員

【Set的遍歷順序就是插入順序】

const s = new Set(['Blue', 'Crazy', 'Pink']);

for(let key of s.keys()){
    console.log(key);
}
/// Blue
// Crazy
// Pink

for(let value of s.values()){
    console.log(value);
}
/// Blue
// Crazy
// Pink

for (let item of s.entries()) {
    console.log(item);
}
// [ 'Blue', 'Blue' ]
// [ 'Crazy', 'Crazy' ]
// [ 'Pink', 'Pink' ]

上面代碼中是前三種方法的遍歷,由於Set數據結構只有鍵值沒有鍵名,所以values和keys的行爲完全一致。

【Set 結構的實例默認可遍歷,它的默認遍歷器生成函數就是它的values方法。】,這意味着,可以省略values方法,直接用for…of循環遍歷 Set。

const s = new Set(['Blue', 'Crazy', 'Pink']);
for (let value of s) {
    console.log(value);
}
/// Blue
// Crazy
// Pink

【forEach( )遍歷】由於Set數據結構和數組很像,所以forEach( )默認對每個值進行操作。

const s = new Set(['Blue', 'Crazy', 'Pink']);
s.forEach(x => console.log(x))
/// Blue
// Crazy
// Pink

上面代碼說明,forEach方法的參數就是一個處理函數。該函數的參數依次爲【鍵值】、【鍵名】、【集合本身】。另外,forEach方法還可以有第二個參數,表示綁定的this對象。

1.2.2.3 遍歷的應用

擴展運算符(…)內部使用for…of循環,所以也可以用於 Set 結構。

const s = new Set(['Blue', 'Crazy', 'Pink']);
let arr = [...s];
//[ 'Blue', 'Crazy', 'Pink' ]

【數組的map和filter方法也可以用於Set數據結構】

let s = new Set([1, 2, 3]);
s = new Set([...s].map(x => x * 2));
console.log(s);
// Set { 2, 4, 6 }

let s = new Set([1, 2, 3, 4]);
s = new Set([...s].filter(x => x % 2 == 0));
console.log(s);
//Set { 2, 4 }

【因此使用 Set 可以很容易地實現並集(Union)、交集(Intersect)和差集】

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

//並集
let union = new Set([...a, ...b]);
// Set { 1, 2, 3, 4 }

//交集
let intersect = new Set([...a].filter(x => b.has(x)));
// Set { 2, 3 }

//差集
let difference = new Set([...a].filter(x => !b.has(x)))
// Set { 1 }

2. WeakSet

WeakSet 結構與 Set 類似,也是不重複的值的集合。但是,它與 Set 有兩個區別。

首先,【WeakSet 的成員只能是對象,而不能是其他類型的值】。

其次,WeakSet 中的對象都是弱引用,即垃圾回收機制不考慮 WeakSet 對該對象的引用,也就是說,如果其他對象都不再引用該對象,那麼垃圾回收機制會自動回收該對象所佔用的內存,【不考慮該對象還存在於 WeakSet 之中】。

由於上面這個特點,WeakSet 的成員是不適合引用的,因爲它會隨時消失。另外,由於 WeakSet 內部有多少個成員,取決於垃圾回收機制有沒有運行,運行前後很可能成員個數是不一樣的,而垃圾回收機制何時運行是不可預測的,因此 ES6 規定 WeakSet 不可遍歷。

2.1 基本語法

WeakSet 是一個構造函數,可以使用new命令,創建 WeakSet 數據結構。

const ws = new WeakSet();

作爲構造函數,WeakSet 可以接受一個數組或類似數組的對象作爲參數。(實際上,任何具有 Iterable 接口的對象,都可以作爲 WeakSet 的參數。)該數組的所有成員,都會自動成爲 WeakSet 實例對象的成員。

const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

上面代碼中,a是一個數組,它有兩個成員,也都是數組。將a作爲 WeakSet 構造函數的參數,a的成員會自動成爲 WeakSet 的成員。

【注意】,是a數組的成員成爲 WeakSet 的成員,而不是a數組本身。這意味着,數組的成員只能是對象。

const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)

上面代碼中,數組b的成員不是對象,加入 WeaKSet 就會報錯。

WeakSet 結構有以下三個方法。

  1. WeakSet.prototype.add(value):向 WeakSet 實例添加一個新成員。
  2. WeakSet.prototype.delete(value):清除 WeakSet 實例的指定成員。
  3. WeakSet.prototype.has(value):返回一個布爾值,表示某個值是否在 WeakSet 實例之中。

下面是一個例子。

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

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

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

ws.delete(window);
ws.has(window);    // false

WeakSet沒有size屬性,沒有辦法遍歷它的成員。

ws.size // undefined
ws.forEach // undefined

ws.forEach(function(item){ console.log('WeakSet has ' + item)})
// TypeError: undefined is not a function

上面代碼試圖獲取size和forEach屬性,結果都不能成功。

WeakSet 不能遍歷,是因爲成員都是弱引用,隨時可能消失,遍歷機制無法保證成員的存在,很可能剛剛遍歷結束,成員就取不到了。WeakSet 的一個用處,是儲存 DOM 節點,而不用擔心這些節點從文檔移除時,會引發內存泄漏。

下面是 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的好處是,foos對實例的引用,不會被計入內存回收機制,所以刪除實例的時候,不用考慮foos,也不會出現內存泄漏。


3. Map

3.1 概述

Map數據結構是ES5鍵值對對象的加強,ES5的鍵值對對象只能使用字符串用作鍵名稱,Map的鍵名範圍不限於字符串,Map結構提供了“值-值”的對應,是一種完善的Hash結構的實現。

const m = new Map();
const key = { name: "Blue" };

m.set(key, 'Crazy');
m.get(key); //Crazy

m.has(key); //true
m.delete(key);  //true
m.has(key); //false

上面代碼中用了Map數據結構的Set方法,將對象key作爲了鍵,然後又使用get方法讀取這個鍵,has方法判斷該結構中是否有該值,然後用delete刪除了這個鍵。

【Map也可以接受一個數組作爲參數】該數組的成員是一個個表示鍵值對的數組。

const m = new Map([
    ["Name", "Blue"],
    ["Age", 23],
    ["Gender", "Man"]
]);
console.log(m);
//  Map { 'Name' => 'Blue', 'Age' => 23, 'Gender' => 'Man' }

上面代碼在構造函數中傳入一個二維數組作爲參數,生成了一個擁有Name、Age、Gender三個鍵的Map數據結構。

Map數據結構接受數組作爲參數實質是執行了下面的操作

const Arr = [
    ["Name", "Blue"],
    ["Age", 23],
    ["Gender", "Man"]
];
const m = new Map();
m.forEach(([key, value]) => m.set(key, value));

上面代碼應用數組的解構賦值,調用本來的set方法爲Map數據結構添加值。

【如果對一個鍵多次賦值,後面的值將覆蓋前面的值】

const m = new Map();
m.set('Name', 'Blue');
m.set('Name', 'Crazy');
console.log(m);
////    Map { 'Name' => 'Crazy' }

【如果讀取一個未知的鍵,返回undefined】

new Map().get('Name');  //undefined

【注意】只有對同一個對象的引用,Map 結構纔將其視爲同一個鍵。

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

上面代碼中的set和get方法,表面上是一個鍵,但實際上這是兩個鍵,這兩個數組雖然數組成員一樣,但是兩個數組在內存中的地址是不一樣的。

那麼同樣值得兩個實例,在Map結構中也被視爲兩個鍵。

const m = new Map(); 
const arr1 = ['a'];
const arr2 = ['a'];
m.set(arr1, 'Blue')
    .set(arr2, 'Crazy');
console.log(m);
////    Map { [ 'a' ] => 'Blue', [ 'a' ] => 'Crazy' }

3.2 實例的屬性和操作方法

3.2.1 實例的屬性

3.2.1.1 size屬性
size屬性在上面中已經接觸到了,返回Map數據結構中的成員總數

const m = new Map();
const arr1 = ['a'];
const arr2 = ['a'];
m.set(arr1, 'Blue')
    .set(arr2, 'Crazy');
m.size; // 2

3.2.2 實例的方法

3.2.2.1 set(key,value)
set方法設置鍵名key對應的鍵值爲value,然後返回整個 Map 結構。如果key已經有值,則鍵值會被更新,否則就新生成該鍵。

const m = new Map();

m.set('edition', 6)        // 鍵是字符串
m.set(262, 'standard')     // 鍵是數值
m.set(undefined, 'nah')    // 鍵是 undefined

set方法 返回的是當前Map對象,因此可以採用鏈式寫法。

let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

3.2.2.2 get(key)
get方法讀取key對應的鍵值,如果找不到key,返回undefined。

const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 鍵是函數

m.get(hello)  // Hello ES6!

3.2.2.2 has(key)
has方法返回一個布爾值,表示某個鍵是否在當前 Map 對象之中。

const m = new Map();

m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');

m.has('edition')     // true
m.has('years')       // false
m.has(262)           // true
m.has(undefined)     // true

3.2.2.3 delete(key)
delete方法刪除某個鍵,返回true。如果刪除失敗,返回false。

const m = new Map();
m.set(undefined, 'nah');
m.has(undefined)     // true

m.delete(undefined)
m.has(undefined)       // false

3.2.2.4 clear( )
clear方法清除所有成員,沒有返回值。

let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0

3.2.2.5 遍歷方法
Map 結構原生提供三個遍歷器生成函數和一個遍歷方法。

  1. keys():返回鍵名的遍歷器。
  2. values():返回鍵值的遍歷器。
  3. entries():返回所有成員的遍歷器。
  4. forEach():遍歷 Map 的所有成員。

【注意】Map的遍歷順序就是插入順序

const m = new Map([
    ['Name', 'Blue'],
    ['Age', 23]
])

for (let key of m.keys()) {
    console.log(key);
}
// Name
// Age

for (let value of m.values()) {
    console.log(value);
}
// Blue
// 23

for (let [key, value] of m.entries()) {
    console.log([key, value]);
}
// [ 'Name', 'Blue' ]
// [ 'Age', 23 ]

【結合擴展運算符,將Map結構轉換成數組】

const m = new Map([
    ['Name', 'Blue'],
    ['Age', 23]
])

console.log([...m.keys()]);
// [ 'Name', 'Age' ]

console.log([...m.values()]);
// [ 'Blue', 23 ]

console.log([...m.entries()]);
// [ [ 'Name', 'Blue' ], [ 'Age', 23 ] ]

console.log([...m]);
// [ [ 'Name', 'Blue' ], [ 'Age', 23 ] ]

4. WeakMap

4.1 概述

WeakMap結構與Map結構類似,也是用於生成鍵值對的集合。

// WeakMap 可以使用 set 方法添加成員
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2

// WeakMap 也可以接受一個數組,
// 作爲構造函數的參數
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

【WeakMap與Map的區別有兩點】

首先,【WeakMap只接受對象作爲鍵名(null除外),不接受其他類型的值作爲鍵名】。

const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key

上面代碼中,如果將數值1和Symbol值作爲 WeakMap 的鍵名,都會報錯。

【其次,WeakMap的鍵名所指向的對象,不計入垃圾回收機制】

WeakMap的設計目的在於,有時我們想在某個對象上面存放一些數據,但是這會形成對於這個對象的引用。請看下面的例子。

const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];

上面代碼中,e1和e2是兩個對象,我們通過arr數組對這兩個對象添加一些文字說明。這就形成了arr對e1和e2的引用。

一旦不再需要這兩個對象,我們就必須手動刪除這個引用,否則垃圾回收機制就不會釋放e1和e2佔用的內存。

// 不需要 e1 和 e2 的時候
// 必須手動刪除引用
arr [0] = null;
arr [1] = null;

上面這樣的寫法顯然很不方便。一旦忘了寫,就會造成內存泄露。

WeakMap 就是爲了解決這個問題而誕生的,它的鍵名所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內。因此,只要所引用的對象的其他引用都被清除,垃圾回收機制就會釋放該對象所佔用的內存。也就是說,一旦不再需要,WeakMap 裏面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用。

基本上,如果你要往對象上添加數據,又不想幹擾垃圾回收機制,就可以使用 WeakMap。一個典型應用場景是,在網頁的 DOM 元素上添加數據,就可以使用WeakMap結構。當該 DOM 元素被清除,其所對應的WeakMap記錄就會自動被移除。

const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

上面代碼中,先新建一個 Weakmap 實例。然後,將一個 DOM 節點作爲鍵名存入該實例,並將一些附加信息作爲鍵值,一起存放在 WeakMap 裏面。這時,WeakMap 裏面對element的引用就是弱引用,不會被計入垃圾回收機制。

也就是說,上面的 DOM 節點對象的引用計數是1,而不是2。這時,一旦消除對該節點的引用,它佔用的內存就會被垃圾回收機制釋放。Weakmap 保存的這個鍵值對,也會自動消失。

總之,WeakMap的專用場合就是,它的鍵所對應的對象,可能會在將來消失。WeakMap結構有助於防止內存泄漏。

注意,WeakMap 弱引用的只是鍵名,而不是鍵值。鍵值依然是正常引用。

const wm = new WeakMap();
let key = {};
let obj = {foo: 1};

wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}

上面代碼中,鍵值obj是正常引用。所以,即使在 WeakMap 外部消除了obj的引用,WeakMap 內部的引用依然存在。

4.2 WeakMap 的語法

WeakMap 與 Map 在 API 上的區別主要是兩個,一是沒有遍歷操作(即沒有key()、values()和entries()方法),也沒有size屬性。因爲沒有辦法列出所有鍵名,某個鍵名是否存在完全不可預測,跟垃圾回收機制是否運行相關。這一刻可以取到鍵名,下一刻垃圾回收機制突然運行了,這個鍵名就沒了,爲了防止出現不確定性,就統一規定不能取到鍵名。二是無法清空,即不支持clear方法。因此,WeakMap只有四個方法可用:get()、set()、has()、delete()。

const wm = new WeakMap();

// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章