文章編寫參考 阮一峯《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實例有以下兩種屬性
- Set.prototype.constructor:構造函數,默認就是Set函數。
- Set.prototype.size:返回Set實例的成員總數。
1.2.2 Set實例的方法
Set實例方法有【操作方法】和【遍歷方法】兩大類。
1.2.2.1 操作方法
- add(value):添加某個值,返回Set結構本身
- delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功。
- has(value):返回一個布爾值,表示該值是否爲Set的成員。
- 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 遍歷方法
- keys():返回鍵名的遍歷器
- values():返回鍵值的遍歷器
- entries():返回鍵值對的遍歷器
- 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 結構有以下三個方法。
- WeakSet.prototype.add(value):向 WeakSet 實例添加一個新成員。
- WeakSet.prototype.delete(value):清除 WeakSet 實例的指定成員。
- 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 結構原生提供三個遍歷器生成函數和一個遍歷方法。
- keys():返回鍵名的遍歷器。
- values():返回鍵值的遍歷器。
- entries():返回所有成員的遍歷器。
- 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