Set數據結構
基本用法
ES6提供了新的數據結構Set。它類似於數組,但是成員的值都是唯一的,沒有重復的值。
Set本身是一個構造函數,用來生成Set數據結構。
var s = new Set(); [2, 3, 5, 4, 5, 2, 2].map(x => s.add(x)); for (let i of s) { console.log(i); } // 2 3 5 4
上面代碼通過add
方法向Set結構加入成員,結果表明Set結構不會添加重復的值。
向Set加入值的時候,不會發生類型轉換,所以5
和"5"
是兩個不同的值。Set內部判斷兩個值是否不同,使用的算法叫做“Same-value equality”,它類似於精確相等運算符(===
),主要的區別是NaN
等於自身,而精確相等運算符認為NaN
不等於自身。
另外,兩個對象總是不相等的。
let set = new Set(); set.add({}); set.size // 1 set.add({}); set.size // 2
上面代碼表示,由於兩個空對象不相等,所以它們被視為兩個值。
Set實例的屬性和方法
Set結構的實例有以下屬性。
Set.prototype.constructor:構造函數,默認就是Set函數。
Set.prototype.size:返回Set實例的成員總數。
Set實例的方法分為兩大類:
- 操作方法(用於操作數據)
- 遍歷方法(用於遍歷成員)
1 操作方法
add(value):添加某個值,返回Set結構本身。
delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功。
has(value):返回一個布爾值,表示該值是否為Set的成員。
clear():清除所有成員,沒有返回值。
2 遍歷方法
Set結構的實例有四個遍歷方法,可以用於遍歷成員。
keys():返回鍵名的遍歷器
values():返回鍵值的遍歷器
entries():返回鍵值對的遍歷器
forEach():使用回調函數遍歷每個成員
需要特別指出的是,Set的遍歷順序就是插入順序。這個特性有時非常有用,比如使用Set保存一個回調函數列表,調用時就能保證按照添加順序調用。
(1)keys(),values(),entries()
keys方法、values方法、entries方法返回的都是遍歷器對象(詳見《Iterator 對象》一章)。由於 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),所以keys方法和values方法的行為完全一致。
上面代碼中,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
(2)forEach()
Set結構的實例的forEach
方法,用於對每個成員執行某種操作,沒有返回值。
let set = new Set([1, 2, 3]); set.forEach((value, key) => console.log(value * 2) ) // 2 // 4 // 6
上面代碼說明,forEach
方法的參數就是一個處理函數。該函數的參數依次為鍵值、鍵名、集合本身(上例省略了該參數)。另外,forEach
方法還可以有第二個參數,表示綁定的this對象。
WeakSet數據結構
WeakSet結構與Set類似,也是不重復的值的集合。但是,它與Set有兩個區別。
首先,WeakSet的成員只能是對象,而不能是其他類型的值。
其次,WeakSet中的對象都是弱引用,即垃圾回收機制不考慮WeakSet對該對象的引用,也就是說,如果其他對象都不再引用該對象,那么垃圾回收機制會自動回收該對象所占用的內存,不考慮該對象還存在於WeakSet之中。這個特點意味着,無法引用WeakSet的成員,因此WeakSet是不可遍歷的。
Map數據結構
Map結構的目的和基本用法
JavaScript的對象(Object),本質上是鍵值對的集合(Hash結構),但是傳統上只能用字符串當作鍵。這給它的使用帶來了很大的限制。
為了解決這個問題,ES6提供了Map數據結構。
Map結構類似於對象,也是鍵值對的集合,但是“鍵”的范圍不限於字符串,各種類型的值(包括對象)都可以當作鍵。也就是說,Object結構提供了“字符串—值”的對應,Map結構提供了“值—值”的對應,是一種更完善的Hash結構實現。如果你需要“鍵值對”的數據結構,Map比Object更合適。
Map的鍵實際上是跟內存地址綁定的,只要內存地址不一樣,就視為兩個鍵。這就解決了同名屬性碰撞(clash)的問題,我們擴展別人的庫的時候,如果使用對象作為鍵名,就不用擔心自己的屬性與原作者的屬性同名。
如果Map的鍵是一個簡單類型的值(數字、字符串、布爾值),則只要兩個值嚴格相等,Map將其視為一個鍵,包括0
和-0
。另外,雖然NaN
不嚴格相等於自身,但Map將其視為同一個鍵。
實例的屬性和操作方法
Map結構的實例有以下屬性和操作方法。
(1)size屬性
size
屬性返回Map結構的成員總數。
(2)set(key, value)
set
方法設置key
所對應的鍵值,然后返回整個Map結構。如果key
已經有值,則鍵值會被更新,否則就新生成該鍵。
set
方法返回的是Map本身,因此可以采用鏈式寫法。
let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c');
(3)get(key)
get
方法讀取key
對應的鍵值,如果找不到key
,返回undefined
。
(4)has(key)
has
方法返回一個布爾值,表示某個鍵是否在Map數據結構中。
(5)delete(key)
delete
方法刪除某個鍵,返回true。如果刪除失敗,返回false。
(6)clear()
clear
方法清除所有成員,沒有返回值。
遍歷方法
Map原生提供三個遍歷器生成函數和一個遍歷方法。
keys():返回鍵名的遍歷器。
values():返回鍵值的遍歷器。
entries():返回所有成員的遍歷器。
forEach():遍歷Map的所有成員。
需要特別注意的是,Map的遍歷順序就是插入順序。
WeakMap
WeakMap
結構與Map
結構基本類似,唯一的區別是它只接受對象作為鍵名(null
除外),不接受其他類型的值作為鍵名,而且鍵名所指向的對象,不計入垃圾回收機制。
WeakMap與Map在API上的區別主要是兩個,一是沒有遍歷操作(即沒有key()
、values()
和entries()
方法),也沒有size
屬性;二是無法清空,即不支持clear
方法。這與WeakMap
的鍵不被計入引用、被垃圾回收機制忽略有關。因此,WeakMap
只有四個方法可用:get()
、set()
、has()
、delete()
。
Iterator和for...of循環
Iterator(遍歷器)的概念
Iterator,本意 迭代,迭代器的意思。
JavaScript原有的表示“集合”的數據結構,主要是數組(Array)和對象(Object),ES6又添加了Map和Set。這樣就有了四種數據集合,用戶還可以組合使用它們,定義自己的數據結構,比如數組的成員是Map,Map的成員是對象。這樣就需要一種統一的接口機制,來處理所有不同的數據結構。
遍歷器(Iterator)就是這樣一種機制。它是一種接口,為各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署Iterator接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。
Iterator的作用有三個:
一是為各種數據結構,提供一個統一的、簡便的訪問接口;
二是使得數據結構的成員能夠按某種次序排列;
三是ES6創造了一種新的遍歷命令for...of
循環,Iterator接口主要供for...of
消費。
Iterator的遍歷過程是這樣的。
(1)創建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
(2)第一次調用指針對象的next
方法,可以將指針指向數據結構的第一個成員。
(3)第二次調用指針對象的next
方法,指針就指向數據結構的第二個成員。
(4)不斷調用指針對象的next
方法,直到它指向數據結構的結束位置。
每一次調用next
方法,都會返回數據結構的當前成員的信息。
由於Iterator只是把接口規格加到數據結構之上,所以,遍歷器與它所遍歷的那個數據結構,實際上是分開的,完全可以寫出沒有對應數據結構的遍歷器對象,或者說用遍歷器對象模擬出數據結構。
遍歷器生成函數,返回一個遍歷器對象(即指針對象)。
在ES6中,有些數據結構原生具備Iterator接口(比如數組),即不用任何處理,就可以被for...of
循環遍歷,有些就不行(比如對象)。原因在於,這些數據結構原生部署了Symbol.iterator
屬性(詳見下文),另外一些數據結構沒有。凡是部署了Symbol.iterator
屬性的數據結構,就稱為部署了遍歷器接口。調用這個接口,就會返回一個遍歷器對象(即通過Symbol.iterator獲取到編輯器生成函數,調用這個函數,就返回一個遍歷器對象)。
數據結構的默認Iterator接口
Iterator接口的目的,就是為所有數據結構,提供了一種統一的訪問機制,即for...of
循環(詳見下文)。
當使用for...of
循環遍歷某種數據結構時,該循環會自動去尋找Iterator接口(即Iterator接口主要供for...of消費)。
一種數據結構只要部署了Iterator接口,我們就稱這種數據結構是”可遍歷的“(iterable)。
ES6規定,默認的Iterator接口部署在數據結構的Symbol.iterator
屬性,或者說,一個數據結構只要具有Symbol.iterator
屬性,就可以認為是“可遍歷的”(iterable)。
Symbol.iterator
屬性本身是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器(遍歷器對象)。至於屬性名Symbol.iterator
,它是一個表達式,返回Symbol
對象的iterator
屬性,這是一個預定義好的、類型為Symbol的特殊值,所以要放在方括號內。
const obj = { [Symbol.iterator] : function () { return { next: function () { return { value: 1, done: true }; } }; } };
上面代碼中,對象obj
是可遍歷的(iterable),因為具有Symbol.iterator
屬性。執行這個屬性,會返回一個遍歷器對象。該對象的根本特征就是具有next
方法。每次調用next
方法,都會返回一個代表當前成員的信息對象,具有value
和done
兩個屬性。
在ES6中,有三類數據結構原生具備Iterator接口:數組、某些類似數組的對象、Set和Map結構。
原生就部署Iterator接口的數據結構有三類,對於這三類數據結構,不用自己寫遍歷器生成函數,for...of
循環會自動遍歷它們。除此之外,其他數據結構(主要是對象)的Iterator接口,都需要自己在Symbol.iterator
屬性上面部署,這樣才會被for...of
循環遍歷。
對於類似數組的對象(存在數值鍵名和length屬性),部署Iterator接口,有一個簡便方法,就是Symbol.iterator
方法直接引用數組的Iterator接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // 或者 NodeList.prototype[Symbol.iterator] = [][Symbol.iterator]; [...document.querySelectorAll('div')] // 可以執行了
如果Symbol.iterator
方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。
調用Iterator接口的場合
有一些場合會默認調用Iterator接口(即Symbol.iterator
方法),除了下文會介紹的for...of
循環,還有幾個別的場合。
(1)解構賦值
(2)擴展運算符
(3)yield*
(4)其他場合
由於數組的遍歷會調用遍歷器接口,所以任何接受數組作為參數的場合,其實都調用了遍歷器接口。下面是一些例子。
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])
) - Promise.all()
- Promise.race()
字符串的Iterator接口
字符串是一個類似數組的對象,也原生具有Iterator接口。
遍歷器對象的return(),throw()
遍歷器對象除了具有next
方法,還可以具有return
方法和throw
方法。如果你自己寫遍歷器對象生成函數,那么next
方法是必須部署的,return
方法和throw
方法是否部署是可選的。
return
方法的使用場合是,如果for...of
循環提前退出(通常是因為出錯,或者有break
語句或continue
語句),就會調用return
方法。如果一個對象在完成遍歷前,需要清理或釋放資源,就可以部署return
方法。
function readLinesSync(file) { return { next() { if (file.isAtEndOfFile()) { file.close(); return { done: true }; } }, return() { file.close(); return { done: true }; }, }; }
上面代碼中,函數readLinesSync
接受一個文件對象作為參數,返回一個遍歷器對象,其中除了next
方法,還部署了return
方法。下面,我們讓文件的遍歷提前返回,這樣就會觸發執行return
方法。
for (let line of readLinesSync(fileName)) { console.log(line); break; }
注意,return
方法必須返回一個對象,這是Generator規格決定的。
for...of循環
一個數據結構只要部署了Symbol.iterator
屬性,就被視為具有iterator接口,就可以用for...of
循環遍歷它的成員。也就是說,for...of
循環內部調用的是數據結構的Symbol.iterator
方法。
for...of
循環可以使用的范圍包括數組、Set 和 Map 結構、某些類似數組的對象(比如arguments
對象、DOM NodeList 對象)、后文的 Generator 對象,以及字符串。
與其他遍歷語法的比較
以數組為例,JavaScript提供多種遍歷語法。最原始的寫法就是for循環。
for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); }
這種寫法比較麻煩,因此數組提供內置的forEach方法。
myArray.forEach(function (value) { console.log(value); });
這種寫法的問題在於,無法中途跳出forEach
循環,break命令或return命令都不能奏效。
for...in
循環可以遍歷數組的鍵名。
for (var index in myArray) { console.log(myArray[index]); }
for...in循環有幾個缺點。
- 數組的鍵名是數字,但是for...in循環是以字符串作為鍵名“0”、“1”、“2”等等。
- for...in循環不僅遍歷數字鍵名,還會遍歷手動添加的其他鍵,甚至包括原型鏈上的鍵。
- 某些情況下,for...in循環會以任意順序遍歷鍵名。
總之,for...in
循環主要是為遍歷對象而設計的,不適用於遍歷數組。
for...of
循環相比上面幾種做法,有一些顯著的優點。
for (let value of myArray) { console.log(value); }
- 有着同for...in一樣的簡潔語法,但是沒有for...in那些缺點。
- 不同用於forEach方法,它可以與break、continue和return配合使用。
- 提供了遍歷所有數據結構的統一操作接口。
下面是一個使用break語句,跳出for...of
循環的例子。
for (var n of fibonacci) { if (n > 1000) break; console.log(n); }
上面的例子,會輸出斐波納契數列小於等於1000的項。如果當前項大於1000,就會使用break語句跳出for...of
循環。