1. 程式人生 > >ES6中的Map集合(與java裏類似)

ES6中的Map集合(與java裏類似)

遍歷 也有 實現 數組 _id getname 私有 而且 click

  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

  上面代碼的setget方法,表面是針對同一個鍵,但實際上這是兩個值,內存地址是不一樣的,因此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

  上面代碼中,變量k1k2的值是一樣的,但是它們在 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集合是唯一的選擇

ES6中的Map集合(與java裏類似)