Set類型可以用來處理列表中的值,但是不適用於處理鍵值對這樣的信息結構。ES6也添加了Map集合來解決類似的問題
一、Map集合
JS的對象(Object),本質上是鍵值對的集合(Hash結構),但是傳統上只能用字符串當作鍵。這給它的使用帶來了很大的限制
為了解決這個問題,ES6提供了Map數據結構。它類似於對象,也是鍵值對的集合,但是“鍵”的范圍不限於字符串,各種類型的值(包括對象)都可以當作鍵。也就是說,Object結構提供了“字符串—值”的對應,Map結構提供了“值—值”的對應,是一種更完善的Hash結構實現
ES6中的Map類型是一種儲存着許多鍵值對的有序列表,其中的鍵名和對應的值支持所有的數據類型。鍵名的等價性判斷是通過調用Object.is()方法實現的,所以數字5與字符串"5"會被判定為兩種類型,可以分別作為獨立的兩個鍵出現在程序中,這一點與對象不一樣,因為對象的屬性名總會被強制轉換成字符串類型
注意:有一個例外,Map集合中將+0和-0視為相等,與Object.is()結果不同
如果需要“鍵值對”的數據結構,Map比Object更合適
1、創建Map集合
如果要向Map集合中添加新的元素,可以調用set()方法並分別傳入鍵名和對應值作為兩個參數;如果要從集合中獲取信息,可以調用get()方法
let map = new Map(); map.set("title", "Understanding ES6"); map.set("year", 2017); console.log(map.get("title")); // "Understanding ES6"
console.log(map.get("year")); // 2017
在這個示例中,兩組鍵值對分別被存入了集合Map中,鍵名"title"對應的值是一個字符串,鍵名"year"對應的值是一個數字。
調用get()方法可以獲得兩個鍵名對應的值。如果調用get()方法時傳入的鍵名在Map集合中不存在,則會返回undefined
在對象中,無法用對象作為對象屬性的鍵名。但是在Map集合中,卻可以這樣做
let map = new Map(), key1 = {}, key2 = {}; map.set(key1, 5); map.set(key2, 42); console.log(map.get(key1)); // 5
console.log(map.get(key2)); // 42
在這段代碼中,分別用對象key1和key2作為兩個鍵名在Map集合里存儲了不同的值。這些鍵名不會被強制轉換成其他形式,所以這兩個對象在集合中是獨立存在的,也就是說,以后不再需要修改對象本身就可以為其添加一些附加信息
2、Map集合支持的方法
在設計語言新標准時,委員會為Map集合與Set集合設計了如下3個通用的方法
(1)has(key)檢測指定的鍵名在Map集合中是否已經存在
(2)delete(key)從Map集合中移除指定鍵名及其對應的值
(3)clear()移除Map集合中的所有鍵值對
Map集合同樣支持size屬性,其代表當前集合中包含的鍵值對數量
let map = new Map(); map.set("name", "huochai"); map.set("age", 25); console.log(map.size); // 2
console.log(map.has("name")); // true
console.log(map.get("name")); // "huochai"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
map.delete("name"); console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.size); // 1
map.clear(); console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.has("age")); // false
console.log(map.get("age")); // undefined
console.log(map.size); // 0
Map集合的size屬性與Set集合中的size屬性類似,其值為集合中鍵值對的數量。在此示例中,首先為Map的實例添加"name"和"age"這兩個鍵名;然后調用has()方法,分別傳入兩個鍵名,返回的結果為true;調用delete()方法移除"name",再用has()方法檢測返回false,且size的屬性值減少1;最后調用clear()方法移除剩余的鍵值對,調用has()方法檢測全部返回false,size屬性的值變為0;clear()方法可以快速清除Map集合中的數據,同樣,Map集合也支持批量添加數據
3、傳入數組來初始化Map集合
可以向Map構造函數傳入數組來初始化一個Map集合,這一點同樣與Set集合相似。數組中的每個元素都是一個子數組,子數組中包含一個鍵值對的鍵名與值兩個元素。因此,整個Map集合中包含的全是這樣的兩元素數組
let map = new Map([["name", "huochai"], ["age", 25]]); console.log(map.has("name")); // true
console.log(map.get("name")); // "huochai"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
console.log(map.size); // 2
初始化構造函數之后,鍵名"name"和"age"分別被添加到Map集合中。數組包裹數組的模式看起來可能有點兒奇怪,但由於Map集合可以接受任意數據類型的鍵名,為了確保它們在被存儲到Map集合中之前不會被強制轉換為其他數據類型,因而只能將它們放在數組中,因為這是唯一一種可以准確地呈現鍵名類型的方式
4、同名屬性碰撞
Map的鍵實際上是跟內存地址綁定的,只要內存地址不一樣,就視為兩個鍵。這就解決了同名屬性碰撞(clash)的問題,擴展別人的庫的時候,如果使用對象作為鍵名,就不用擔心自己的屬性與原作者的屬性同名
const map = new Map(); map.set(['a'], 555); map.get(['a']) // undefined
上面代碼的set
和get
方法,表面是針對同一個鍵,但實際上這是兩個值,內存地址是不一樣的,因此get
方法無法讀取該鍵,返回undefined
const map = new Map(); const k1 = ['a']; const k2 = ['a']; map .set(k1, 111) .set(k2, 222); map.get(k1) // 111
map.get(k2) // 222
上面代碼中,變量k1
和k2
的值是一樣的,但是它們在 Map 結構中被視為兩個鍵
5、遍歷
Map結構原生提供三個遍歷器生成函數和一個遍歷方法
keys():返回鍵名的遍歷器
values():返回鍵值的遍歷器
entries():返回所有成員的遍歷器
forEach():遍歷 Map 的所有成員
注意:Map的遍歷順序就是插入順序
const map = new Map([ ['F', 'no'], ['T', 'yes'], ]); for (let key of map.keys()) { console.log(key); } // "F" // "T"
for (let value of map.values()) { console.log(value); } // "no" // "yes"
for (let item of map.entries()) { console.log(item[0], item[1]); } // "F" "no" // "T" "yes" // 或者
for (let [key, value] of map.entries()) { console.log(key, value); } // "F" "no" // "T" "yes" // 等同於使用map.entries()
for (let [key, value] of map) { console.log(key, value); } // "F" "no" // "T" "yes"
上面代碼最后的那個例子,表示Map結構的默認遍歷器接口,就是entries
方法
map[Symbol.iterator] === map.entries// true
6、轉為數組
Map結構轉為數組結構,比較快速的方法是使用擴展運算符(...
)
const map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); [...map.keys()] // [1, 2, 3]
[...map.values()] // ['one', 'two', 'three']
[...map.entries()] // [[1,'one'], [2, 'two'], [3, 'three']]
[...map] // [[1,'one'], [2, 'two'], [3, 'three']]
結合數組的map
方法、filter
方法,可以實現 Map 的遍歷和過濾
const map0 = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c'); const map1 = new Map( [...map0].filter(([k, v]) => k < 3) ); // 產生 Map 結構 {1 => 'a', 2 => 'b'}
const map2 = new Map( [...map0].map(([k, v]) => [k * 2, '_' + v]) ); // 產生 Map 結構 {2 => '_a', 4 => '_b', 6 => '_c'}
7、forEach()
Map還有一個forEach
方法,與數組的forEach
方法類似,也可以實現遍歷
const map = new Map([[1, 'one'],[2, 'two'],[3, 'three']]); map.forEach((value,key,map)=>{ //one 1 {1 => "one", 2 => "two", 3 => "three"} //two 2 {1 => "one", 2 => "two", 3 => "three"} //three 3 {1 => "one", 2 => "two", 3 => "three"}
console.log(value,key,map); })
注意:遍歷過程中,Map會按照鍵值對插入Map集合的順序將相應信息傳入forEach()方法的回調函數;而在數組中,會按照數值型索引值的順序依次傳入回調函數
forEach
方法還可以接受第二個參數,用來綁定this
const reporter = { report: function(key, value) { console.log("Key: %s, Value: %s", key, value); } }; map.forEach(function(value, key, map) { this.report(key, value); }, reporter);
上面代碼中,forEach
方法的回調函數的this
,就指向reporter
二、WeakMap
WeakSet是引用Set集合,相對地,WeakMap是弱引用Map集合,也用於存儲對象的弱引用
WeakMap集合中的鍵名必須是一個對象,如果使用非對象鍵名會報錯;集合中保存的是這些對象的弱引用,如果在弱引用之外不存在其他的強引用,引擎的垃圾回收機制會自動回收這個對象,同時也會移除WeakMap集合中的鍵值對。但是只有集合的鍵名遵從這個規則,鍵名對應的值如果是一個對象,則保存的是對象的強引用,不會觸發垃圾回收機制
WeakMap集合最大的用途是保存Web頁面中的DOM元素,例如,一些為Web頁面打造的JS庫,會通過自定義的對象保存每一個引用的DOM元素
使用這種方法最困難的是,一旦從Web頁面中移除保存過的DOM元素,如何通過庫本身將這些對象從集合中清除;否則,可能由於庫過於龐大而導致內存泄露,最終程序不再正常執行。如果用WeakMap集合來跟蹤DOM元素,這些庫仍然可以通過自定義的對象整合每一個DOM元素,而且當DOM元素消失時,可以自動銷毀集合中的相關對象
1、使用WeakMap集合
ES6中的Weak Map類型是一種存儲着許多鍵值對的無序列表,列表的鍵名必須是非null類型的對象,鍵名對應的值則可以是任意類型。WeakMap的接口與Map非常相似,通過set()方法添加數據,通過get()方法獲取數據
let map = new WeakMap(), element = document.querySelector(".element"); map.set(element, "Original"); let value = map.get(element); console.log(value); // "Original" // 移除元素
element.parentNode.removeChild(element); element = null; // 該 Weak Map 在此處為空
在這個示例中儲存了一個鍵值對,鍵名element是一個DOM元素,其對應的值是一個字符串,將DOM元素傳入get()方法即可獲取之前存過的值。如果隨后從document對象中移除DOM元素並將引用這個元素的變量設置為null,那么WeakMap集合中的數據也會被同步清除
與WeakSet集合相似的是,WeakMap集合也不支持size屬性,從而無法驗證集合是否為空;同樣,由於沒有鍵對應的引用,因而無法通過get()方法獲取到相應的值,WeakMap集合自動切斷了訪問這個值的途徑,當垃圾回收程序運行時,被這個值占用的內存將會被釋放
2、WeakMap集合的初始化方法
WeakMap集合的初始化過程與Map集合類似,調用WeakMap構造函數並傳入一個數組容器,容器內包含其他數組,每一個數組由兩個元素構成:第一個元素是一個鍵名,傳入的值必須是非null的對象;第二個元素是這個鍵對應的值(可以是任意類型)
let key1 = {}, key2 = {}, map = new WeakMap([[key1, "Hello"], [key2, 42]]); console.log(map.has(key1)); // true
console.log(map.get(key1)); // "Hello"
console.log(map.has(key2)); // true
console.log(map.get(key2)); // 42
對象key1和key2被當作WeakMap集合的鍵使用,可以通過get()方法和has()方法去訪問。如果給WeakMap構造函數傳入的諸多鍵值對中含有非對象的鍵,會導致程序拋出錯誤
3、WeakMap集合支持的方法
WeakMap集合只支持兩個可以操作鍵值對的方法:
has()方法可以檢測給定的鍵在集合中是否存在;
delete()方法可移除指定的鍵值對。
WeakMap集合與WeakSet集合一樣,都不支持鍵名枚舉,從而也不支持clear()方法
let map = new WeakMap(), element = document.querySelector(".element"); map.set(element, "Original"); console.log(map.has(element)); // true
console.log(map.get(element)); // "Original"
map.delete(element); console.log(map.has(element)); // false
console.log(map.get(element)); // undefined
在這段代碼中,我們還是用DOM元素作為Weak Map集合的鍵名。has()方法可以用於檢查Weak Map集合中是否存在指定的引用;Weak Map集合的鍵名只支持非null的對象值;調用delete()方法可以從Weak Map集合中移除指定的鍵值對,此時如果再調用has()方法檢查這個鍵名會返回false,調用get()方法返回undefined
4、用途
(1)儲存DOM元素:前面介紹過,WeakMap應用的典型場合就是 DOM 節點作為鍵名
let myElement = document.getElementById('logo'); let myWeakmap = new WeakMap(); myWeakmap.set(myElement, {timesClicked: 0}); myElement.addEventListener('click', function() { let logoData = myWeakmap.get(myElement); logoData.timesClicked++; }, false);
上面代碼中,myElement
是一個 DOM 節點,每當發生click
事件,就更新一下狀態。我們將這個狀態作為鍵值放在WeakMap里,對應的鍵名就是myElement
。一旦這個 DOM 節點刪除,該狀態就會自動消失,不存在內存泄漏風險
進一步說,注冊監聽事件的listener
對象,就很合適用 WeakMap 實現
const listener = new WeakMap(); listener.set(element1, handler1); listener.set(element2, handler2); element1.addEventListener('click', listener.get(element1), false); element2.addEventListener('click', listener.get(element2), false);
上面代碼中,監聽函數放在WeakMap里面。一旦 DOM 對象消失,跟它綁定的監聽函數也會自動消失
(2)部署私有屬性:WeakMap的另一個用處是部署私有屬性
function Person(name) { this._name = name; } Person.prototype.getName = function() { return this._name; };
在這段代碼中,約定前綴為下划線_的屬性為私有屬性,不允許在對象實例外改變這些屬性。例如,只能通過getName()方法讀取this._name屬性,不允許改變它的值。然而沒有任何標准規定如何寫_name屬性,所以它也有可能在無意間被覆寫
在ES5中,可以通過以下這種模式創建一個對象接近真正的私有數據
var Person = (function() { var privateData = {}, privateId = 0; function Person(name) { Object.defineProperty(this, "_id", { value: privateId++ }); privateData[this._id] = { name: name }; } Person.prototype.getName = function() { return privateData[this._id].name; }; return Person; }());
在上面的示例中,變量person由一個立即調用函數表達式(IIFE)生成,包括兩個私有變量privateData和privateld。privateData對象儲存的是每一個實例的私有信息,privateld則為每個實例生成一個獨立ID。當調用person構造函數時,屬性_id的值會被加1,這個屬性不可枚舉、不可配置並且不可寫
然后,新的條目會被添加到privateData對象中,條目的鍵名是對象實例的ID;privateData對象中儲存了所有實例對應的名稱。調用getName()函數,即可通過this_id獲得當前實例的ID,並以此從privateData對象中提取實例名稱。在IIFE外無法訪問privateData對象,即使可以訪問this._id,數據實際上也很安全
這種方法最大的問題是,如果不主動管理,由於無法獲知對象實例何時被銷毀,因此privateData中的數據就永遠不會消失。而使用WeakMap集合可以解決這個問題
let Person = (function() { let privateData = new WeakMap(); function Person(name) { privateData.set(this, { name: name }); } Person.prototype.getName = function() { return privateData.get(this).name; }; return Person; }());
經過改進后的Person構造函數選用一個WeakMap集合來存放私有數據。由於Person對象的實例可以直接作為集合的鍵使用,無須單獨維護一套ID的體系來跟蹤數據。調用Person構造函數時,新條目會被添加到WeakMap集合中,條目的鍵是this,值是對象包含的私有信息。在這個示例中,值是一個包含name屬性的對象。調用getName()函數時會將this傳入privateData.get()方法作為參數獲取私有信息,亦即獲取value對象並且訪問name屬性。只要對象實例被銷毀,相關信息也會被銷毀,從而保證了信息的私有性
5、使用方式及使用限制
要在WeakMap集合與普通的Map集合之間做出選擇時,需要考慮的主要問題是,是否只用對象作為集合的鍵名。如果是,那么Weak Map集合是最好的選擇。當數據再也不可訪問后,集合中存儲的相關引用和數據都會被自動回收,這有效地避免了內存泄露的問題,從而優化了內存的使用
相對Map集合而言,WeakMap集合對用戶的可見度更低,其不支持通過forEach()方法、size屬性及clear()方法來管理集合中的元素。如果非常需要這些特性,那么Map集合是一個更好的選擇,只是一定要留意內存的使用情況
當然,如果只想使用非對象作為鍵名,那么普通的Map集合是唯一的選擇