前面的話
在ES6標准制定以前,由於可選的集合類型有限,數組使用的又是數值型索引,因而經常被用於創建隊列和棧。如果需要使用非數值型索引,就會用非數組對象創建所需的數據結構,而這就是Set集合與Map集合的早期實現。本文將詳細介紹ES6中的set和map集合
引入
Set集合是一種無重復元素的列表,開發者們一般不會逐一讀取數組中的元素,也不太可能逐一訪問Set集合中的每個元素,通常的做法是檢測給定的值在某個集合中是否存在
Map集合內含多組鍵值對,集合中每個元素分別存放着可訪問的鍵名和它對應的值,Map集合經常被用於緩存頻繁取用的數據。在標准正式發布以前,開發者們已經在ES5中用非數組對象實現了類似的功能
ES6新標准將Set集合與Map集合添加到JS中
【ES5】
在ES5中,開發者們用對象屬性來模擬這兩種集合
let set = Object.create(null); set.foo = true; // 檢查屬性的存在性 if (set.foo) { // 一些操作 }
這里的變量set是一個原型為null的對象,不繼承任何屬性。在ES5中,開發者們經常用類似的方法檢查對象的某個屬性值是否存在。 在這個示例中,將set.foo賦值為true,通過if語句可以確認該值存在於當前對象中
模擬這兩種集合對象的唯一區別是存儲的值不同,以下這個示例是用對象模擬Map集合
let map = Object.create(null); map.foo = "bar"; // 提取一個值 let value = map.foo; console.log(value); // "bar"
這段代碼將字符串"bar"儲存在map.foo中。一般來說,Set集合常被用於檢查對象中是否存在某個鍵名,而Map集合常被用於獲取已存的信息
如果程序很簡單,確實可以用對象來模擬Set集合與Map集合,但如果觸碰到對象屬性的某些限制,那么這個方法就會變得更加復雜。例如,所有對象的屬性名必須是字符串類型,必須確保每個鍵名都是字符串類型且在對象中是唯一的
let map = Object.create(null); map[5] = "foo"; console.log(map["5"]); // "foo"
本例中將對象的某個屬性賦值為字符串"foo",而這個屬性的鍵名是數值型的5,它會被自動轉換成字符串,所以map["5"]和map[5]引用的其實是同一個屬性。如果想分別用數字和字符串作為對象屬性的鍵名,則內部的自動轉換機制會導致很多問題。當然,用對象作為屬性的鍵名也會遇到類似的問題
let map = Object.create(null), key1 = {}, key2 = {}; map[key1] = "foo"; console.log(map[key2]); // "foo"
由於對象屬性的鍵名必須是字符串,因而這段代碼中的key1和key2將被轉換為對象對應的默認字符串"[object Object]",所以map[key2]和map[key1]引用的是同一個屬性。這種錯誤很難被發現,用不同對象作為對象屬性的鍵名理論上應該指向多個屬性,但實際上這種假設卻不成立
由於對象會被轉換為默認的字符串表達方式,因此其很難用作對象屬性的鍵名
對於Map集合來說,如果它的屬性值是假值,則在要求使用布爾值的情況下(例如在if語句中)會被自動轉換成false。強制轉換本身沒有問題,但如果考慮這個值的使用場景,就有可能導致錯誤發生
let map = Object.create(null); map.count = 1; // 是想檢查 "count" 屬性的存在性,還是想檢查非零值? if (map.count) { // ... }
這個示例中有一些模棱兩可的地方,比如我們應該怎樣使用map.count?if語句中,我們是檢查map.count是否存在,還是檢查值是否非零。在示例中,由於value的值是1,為真值,if語句中的代碼將被執行。然而,如果map.count的值為0或者不存在,if語句中的代碼塊將不會被執行
在大型軟件應用中,一旦發生此類問題將難以定位及調試,從而促使ES6在語言中加入Set集合與Map集合這兩種新特性
當然,在JS中有一個in運算符,不需要讀取對象的值就可以判斷屬性在對象中是否存在,如果存在就返回true。但是,in運算符也會檢索對象的原型,只有當對象原型為null時使用這個方法才比較穩妥
Set集合
ES6 提供了新的數據結構 Set。它類似於數組,但是成員的值都是唯一的,沒有重復的值。通過Set集合可以快速訪問其中的數據,更有效地追蹤各種離散值
Set 結構的實例有以下屬性
Set.prototype.constructor:構造函數,默認就是Set函數
Set.prototype.size:返回Set實例的成員總數
Set 實例的操作方法(用於操作數據)包括以下4個
add(value):添加某個值,返回Set結構本身 has(value):返回一個布爾值,表示該值是否為Set的成員 delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功 clear():清除所有成員,沒有返回值
【創建Set集合、add()添加元素】
調用new Set()創建Set集合,調用add()方法向集合中添加元素,訪問集合的size屬性可以獲取集合中目前的元素數量
let set = new Set(); set.add(5); set.add("5"); console.log(set.size); // 2
在Set集合中,不會對所存值進行強制的類型轉換,數字5和字符串"5"可以作為兩個獨立元素存在
const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); } // 2 3 5 4
上面代碼通過add
方法向 Set 結構加入成員,結果表明 Set 結構不會添加重復的值。
當然,如果向Set集合中添加多個對象,則它們之間彼此保持獨立
let set = new Set(), key1 = {}, key2 = {}; set.add(key1); set.add(key2); console.log(set.size); // 2
由於key1和key2不會被轉換成字符串,因而它們在Set集合中是兩個獨立的元素;如果被轉換, 則二者的值都是'[object Object]'
如果多次調用add()方法並傳入相同的值作為參數,那么后續的調用實際上會被忽略
let set = new Set(); set.add(5); set.add("5"); set.add(5); // 重復了,該調用被忽略 console.log(set.size); // 2
由於第二次傳入的數字5是一個重復值,因此其不會被添加到集合中,所以控制台最后輸出的Set集合size屬性值為2
可以使用數組來初始化一個 Set ,並且 Set 構造器會確保不重復地使用這些值
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]); console.log(set.size); // 5
在這個示例中,我們用一個含重復元素的數組來初始化Set集合,數組中有4個數字5,而在生成的集合中只有一個。自動去重的功能對於將已有代碼或JSON結構轉換為Set集合執行得非常好
實際上,Set構造函數可以接受所有可迭代對象作為參數,數組、Set集合、Map集合都是可迭代的,因而都可以作為Set構造函數的參數使用;構造函數通過迭代器從參數中提取值
【has()檢測元素】
通過has()方法可以檢測Set集合中是否存在某個值
let set = new Set(); set.add(5); set.add("5"); console.log(set.has(5)); // true console.log(set.has(6)); // false
在這段代碼中,set集合里沒有數字6這個值,所以set.has(6)調用返回false
【delete()和clear()移除元素】
調用delete()方法可以移除Set集合中的某一個元素,調用clear()方法會移除集合中的所有元素
let set = new Set(); set.add(5); set.add("5"); console.log(set.has(5)); // true set.delete(5); console.log(set.has(5)); // false console.log(set.size); // 1 set.clear(); console.log(set.has("5")); // false console.log(set.size); // 0
調用delete(5)之后,只有數字5被移除;執行clear()方法后,Set集合中的所有元素都被清除了
【遍歷操作】
Set 結構的實例有四個遍歷方法,可以用於遍歷成員
keys():返回鍵名的遍歷器
values():返回鍵值的遍歷器
entries():返回鍵值對的遍歷器
forEach():使用回調函數遍歷每個成員
keys()、values()、entries()
keys
方法、values
方法、entries
方法返回的都是遍歷器對象。由於 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),所以keys
方法和values
方法的行為完全一致
let set = new Set(['red', 'green', 'blue']); for (let item of set.keys()) { console.log(item); } // red // green // blue for (let item of set.values()) { console.log(item); } // red // green // blue for (let item of set.entries()) { console.log(item); } // ["red", "red"] // ["green", "green"] // ["blue", "blue"]
上面代碼中,entries
方法返回的遍歷器,同時包括鍵名和鍵值,所以每次輸出一個數組,它的兩個成員完全相等
Set 結構的實例默認可遍歷,它的默認遍歷器生成函數就是它的values
方法
Set.prototype[Symbol.iterator] === Set.prototype.values// true
這意味着,可以省略values
方法,直接用for...of
循環遍歷 Set
let set = new Set(['red', 'green', 'blue']); for (let x of set) { console.log(x); } // red // green // blue
forEach()
Set結構的實例的forEach
方法,用於對每個成員執行某種操作,沒有返回值
let set = new Set(['a','b','c']); set.forEach((key, value, set) => { console.log(key,value,set);} ) //a a ['a','b','c'] //b b ['a','b','c'] //c c ['a','b','c']
上面代碼說明,forEach
方法的參數就是一個處理函數。該函數的參數依次為鍵值、鍵名、集合本身
在Set集合的forEach()方法中,第二個參數也與數組的一樣,如果需要在回調函數中使用this引用,則可以將它作為第二個參數傳入forEach()函數
let set = new Set([1, 2]); let processor = { output(value) { console.log(value); }, process(dataSet) { dataSet.forEach(function(value) { this.output(value); }, this); } }; processor.process(set);
以上示例中,processor.process()方法調用了Set集合的forEach()方法並將this傳入作為回調函數的this值,從而this.output()方法可以正確調用processor.output()方法。forEach()方法的回調函數只使用了第一個參數value,所以直接省略了其他參數。在這里也可以使用箭頭函數,這樣就無須再將this作為第二個參數傳入回調函數了
let set = new Set([1, 2]); let processor = { output(value) { console.log(value); }, process(dataSet) { dataSet.forEach((value) => this.output(value)); } }; processor.process(set);
在此示例中,箭頭函數從外圍的process()函數讀取this值,所以可以正確地將this.output()方法解析為一次processor.output()調用
[注意]盡管Set集合更適合用來跟蹤多個值,而且又可以通過forEach()方法操作集合中的每一個元素,但是不能像訪問數組元素那樣直接通過索引訪問集合中的元素。如有需要,最好先將Set集合轉換成一個數組
【將Set集合轉換為數組】
將數組轉換為Set集合的過程很簡單,只需給Set構造函數傳入數組即可;將Set集合再轉回數組的過程同樣很簡單,需要用到展開運算符(...),它可以將數組中的元素分解為各自獨立的函數參數。展開運算符也可以將諸如Set集合的可迭代對象轉換為數組
let set = new Set([1, 2, 3, 3, 3, 4, 5]), array = [...set]; console.log(array); // [1,2,3,4,5]
在這里,用一個含重復元素的數組初始化Set集合,集合會自動移除這些重復元素然后再用展開運算符將這些元素放到一個新的數組中。Set集合依然保留創建時接受的元素(1、2、3、4、5),新數組中保存着這些元素的副本
如果已經創建過一個數組,想要復制它並創建一個無重復元素的新數組,則上述這個方法就非常有用
function eliminateDuplicates(items) { return [...new Set(items)]; } let numbers = [1, 2, 3, 3, 3, 4, 5], noDuplicates = eliminateDuplicates(numbers); console.log(noDuplicates); // [1,2,3,4,5]
在以上函數中,Set集合僅是用來過濾重復值的臨時中介,最后會輸出新創建的無重復元素的數組
WeakSet
將對象存儲在Set的實例與存儲在變量中完全一樣,只要Set實例中的引用存在,垃圾回收機制就不能釋放該對象的內存空間,於是之前提到的Set類型可以被看作是一個強引用的Set集合
let set = new Set(), key = {}; set.add(key); console.log(set.size); // 1 // 取消原始引用 key = null; console.log(set.size); // 1 // 重新獲得原始引用 key = [...set][0];
在這個示例中,將變量key設置為null時便清除了對初始對象的引用,但是Set集合卻保留了這個引用,仍然可以使用展開運算符將Set集合轉換成數組格式並從數組的首個元素取出該引用
大部分情況下這段代碼運行良好,但有時候會希望當其他所有引用都不再存在時,讓Set集合中的這些引用隨之消失。舉個例子,如果在Web頁面中通過JS代碼記錄了一些DOM元素,這些元素有可能被另一段腳本移除,而又不希望自己的代碼保留這些DOM元素的最后一個引用
為了解決這個問題,ES6中引入了另外一個類型:WeakSet集合(弱引用Set集合)
【創建WeakSet集合】
用Weakset構造函數可以創建WeakSet集合,集合支持3個方法:add()、has()和delete()
let set = new WeakSet(), key = {}; // 將對象加入 set set.add(key); console.log(set.has(key)); // true set.delete(key); console.log(set.has(key)); // false
WeakSet集合的使用方式與Set集合類似,可以向集合中添加引用,從中移除引用,也可以檢査集合中是否存在指定對象的引用。也可以調用WeakSet構造函數並傳入一個可迭代對象來創建WeakSet集合
let key1 = {}, key2 = {}, set = new WeakSet([key1, key2]); console.log(set.has(key1)); // true console.log(set.has(key2)); // true
以上示例中,向WeakSet構造函數傳入一個含有兩個對象的數組,最終創建包含這兩個對象的WeakSet集合
【與Set集合的區別】
WeakSet與Set最大的區別是WeakSet中的對象都是弱引用,即垃圾回收機制不考慮WeakSet對該對象的引用,也就是說,如果其他對象都不再引用該對象,那么垃圾回收機制會自動回收該對象所占用的內存,不考慮該對象還存在於WeakSet之中
let set = new WeakSet(), key = {}; set.add(key); console.log(set.has(key)); // true // 取消原始引用 key = null; console.log(set.has(key)); // false
由於上面這個特點,WeakSet的成員是不適合引用的,因為它會隨時消失。另外,由於WeakSet內部有多少個成員,取決於垃圾回收機制有沒有運行,運行前后很可能成員個數是不一樣的,而垃圾回收機制何時運行是不可預測的,因此ES6規定WeakSet不可遍歷
除了以上主要區別之外,它們之間還有下面幾個差別
1、在Weakset的實例中,如果向add()、has()和delete()這3個方法傳入非對象參數都會導致程序報錯
2、WeakSet集合不可迭代,所以不能被用於for-of循環
3、WeakSet集合不暴露任何迭代器(例如keys()和values()方法),所以無法通過程序本身來檢測其中的內容
4、WeakSet集合不支持forEach()方法
5、WeakSet集合不支持size屬性
WeakSet集合的功能看似受限,其實這是為了讓它能夠正確地處理內存中的數據。總之,如果只需要跟蹤對象引用,更應該使用Weak Set集合而不是普通的Set集合
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更合適
【創建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集合里存儲了不同的值。這些鍵名不會被強制轉換成其他形式,所以這兩個對象在集合中是獨立存在的,也就是說,以后不再需要修改對象本身就可以為其添加一些附加信息
【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集合也支持批量添加數據
【傳入數組來初始化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集合中之前不會被強制轉換為其他數據類型,因而只能將它們放在數組中,因為這是唯一一種可以准確地呈現鍵名類型的方式
【同名屬性碰撞】
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 結構中被視為兩個鍵
【遍歷】
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
轉為數組
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'}
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元素消失時,可以自動銷毀集合中的相關對象
【使用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集合自動切斷了訪問這個值的途徑,當垃圾回收程序運行時,被這個值占用的內存將會被釋放
【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構造函數傳入的諸多鍵值對中含有非對象的鍵,會導致程序拋出錯誤
【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
【用途】
儲存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 對象消失,跟它綁定的監聽函數也會自動消失
部署私有屬性
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屬性。只要對象實例被銷毀,相關信息也會被銷毀,從而保證了信息的私有性
【使用方式及使用限制】
要在WeakMap集合與普通的Map集合之間做出選擇時,需要考慮的主要問題是,是否只用對象作為集合的鍵名。如果是,那么Weak Map集合是最好的選擇。當數據再也不可訪問后,集合中存儲的相關引用和數據都會被自動回收,這有效地避免了內存泄露的問題,從而優化了內存的使用
相對Map集合而言,WeakMap集合對用戶的可見度更低,其不支持通過forEach()方法、size屬性及clear()方法來管理集合中的元素。如果非常需要這些特性,那么Map集合是一個更好的選擇,只是一定要留意內存的使用情況
當然,如果只想使用非對象作為鍵名,那么普通的Map集合是唯一的選擇