遍歷得到數組 Or Iterator 遍歷器?
一、背景
故事的開頭是這樣的...
在遍歷數組與對象屬性時,對使用 obj.keys()、obj.values()和 obj.entries() 還是 Object.keys(obj)、Object.values(obj)、Object.entries(obj)方法產生了一些困惑。話不多說,先放問題:
需求:想要遍歷一個對象,并獲取遍歷對象的屬性值 實現:Object.keys()、Object.values() 和 Object.entries() 方法 問題:一不小心同數組的 entries(),keys()和 values() 方法混淆了~QAQ
二、keys()、values()、entries()遍歷方法
熟悉 ES 語法數據結構的朋友一定很清楚,原生對象數據結構并不支持 obj.keys()、obj.values()和 obj.entries() 方法,數組與 map、set 等數據結構才支持。但仍可以通過 Object.keys(obj)、Object.values(obj)、Object.entries(obj)獲取原生對象中可遍歷的屬性組成數組類型數據結構。
也就是說,keys()、values()和 entries() 方法有兩種:
ES5-ES2017 相繼引入 Object.keys 、Object.values 和 Object.entries 方法,返回一個數組,成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵名/鍵值/鍵值對,可以用 for...of 循環進行遍歷;
ES6 提供 entries(),keys() 和 values() -- 可用于遍歷數組/Map/Set 等類數組數據結構實例,返回一個(Iterator)遍歷器對象,可以用 for...of 循環進行遍歷。
注意這里又有兩點區別:
兩者調用語法不同,顯而易見;
前者返回的是一個可迭代的對象,而后者返回的是一個真正的數組。
有沒有被繞暈?那我們先來看第一個問題吧 -- 調用語法的不同
Q1: Object.keys 、Object.values 和 Object.entries 方法
為了區分這兩種調用語法,我們必須得來回顧下原型鏈的相關知識。
因為這里的 entries(),keys()和 values() 方法正是是調用原型對象構造函數上的方法。如下圖可以看到,對于一個普通對象,這三個方法在 Object 對象的[[prototype]]下的 constructor 中:
而對于一個數組結構來說,這三個方法可以在數組原型鏈中和原型鏈上層對象原型的 constructor 中同時找到:
即 Object.keys(arr)調用的是數組原型鏈頂層原型對象 constructor 的方法,而數組本身也支持的 arr.keys()方法,則是調用數組原型鏈上的方法。
即對象只支持前種調用方式,而數組同時支持這兩種調用:
同時我們知道在 JavaScript 中,對象是所有復雜結構的基礎。也正對應了其他復雜結構原型鏈的頂端是對象原型結構。現在應該能夠知道為何普通對象不支持 obj.keys()、obj.values()和 obj.entries() 方法了,但到這里就不得不提出另一個疑問了:
Q2: 如何讓一個對象支持 obj.keys()、obj.values()和 obj.entries() 方法呢?
理論上,我們是可以為一個對象構造任意方法,那么如何實現和數組一樣的遍歷方法呢?本質上這個方法是能夠生成一個遍歷器。
- let objE = {
- data: [ 'hello', 'world' ],
- keys: function() {
- const self = this;
- return {
- [Symbol.iterator]() {
- let index = 0;
- return {
- next() {
- if (index < self.data.length) {
- return {
- value: self.data[index++],
- done: false
- };
- }
- return { value: undefined, done: true };
- }
- };
- }
- }
- }
- };
上述,我們自己創建了一個 data 對象,并實現了它自己的 data.values() 方法。同時,我們依然可以對它調用 Object.values(data) 方法。
從上面的方法不難看出,我們在對象中通過添加 Symbol.iterator 手動構造了一個輸出遍歷器函數,關于遍歷器的討論我們在下一節討論,現在先來討論調用返回結果的區別。
Q3: 兩種調用方法返回結果:遍歷器與數組
1)第一種調用方法,根據定義可知:返回一個數組,數組成員是參數對象自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵名/鍵值/鍵值對。
敲重點!!!這三個方法只返回對象自身的可遍歷屬性,即屬性描述對象的 enumerable 為 true。
我們可以通過 for ... in 循環來實現相同的遍歷效果。
2)而第二種方法,返回一個遍歷器:顧名思義,遍歷器也可以滿足循環遍歷的需求。
本質上,遍歷器的定義是一種接口,為各種不同的數據結構提供統一的訪問機制。接下來就來了解下適用于不同數據結構的遍歷器。
三、Iterator 遍歷器
首先我們知道,目前主要有四種表示“集合”的數據結構:數組(Array)、對象(Object)、Map 和 Set,這里表示"集合"的對象例如 NodeList 集合類數組對象,而遍歷器可以使我們遍歷訪問這些集合。
實際上,原生具備 Iterator 接口的數據結構包括 Array、Map、Set、String、TypedArray、函數的 arguments 對象和 NodeList 對象。
具體遍歷器的概念可參考阮一峰老師 ES6 入門 Iterator 一章,已經十分詳細清楚:
因此,Iterator 遍歷器本質上為所有數據結構,提供了一種統一的訪問機制,即 for...of 循環。
關于遍歷,我們前面已經講到了遍歷對象屬性,這里再提一嘴:
1. 遍歷類數組對象/Array/Map/Set 等數組數據結構實例
當使用 for...of 循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。一種數據結構只要部署了 Iterator 接口,我們就稱這種數據結構是“可遍歷的”(iterable)。ES6 規定,默認的 Iterator 接口部署在數據結構的 Symbol.iterator 屬性,換句話說,一個數據結構只要具有 Symbol.iterator 屬性,就可以認為是“可迭代/遍歷的”(iterable)。
2. 獲取對象可遍歷屬性
Object.keys 、Object.values 和 Object.entries 方法只返回對象自身的可遍歷屬性,通過屬性描述對象的 enumerable 標識改對象屬性是否可以遍歷。同時因為普通對象 not iterable,即普通對象不具有 Symbol.iterator 屬性,所以無法通過 for...of 循環直接遍歷,否則會報錯 Uncaught TypeError: obj is not iterable。
可見,數組及類數組的遍歷(迭代)與普通對象中的提到的遍歷是不同的,這分別取決于各自的 iterable 和 enumerable 屬性。
3. for ... of
ES6 中引入 for...of 循環,很多時候用以替代 for...in 和 forEach() ,并支持新的迭代協議。for...of 語句在可迭代對象上創建一個迭代循環,調用自定義迭代鉤子,并為每個不同屬性的值執行語句。
那么終極問題:如何實現 Symbol.iterator 方法,使普通對象可被 for of 迭代?其實在 Q2 部分已經實現了。
嘗試給普通對象實現一個 Symbol.iterator 接口:
- // 普通對象
- const obj = {
- foo: 'value1',
- bar: 'value2',
- [Symbol.iterator]() {
- // 這里 Object.keys 不會獲取到 Symbol.iterator 屬性
- const keys = Object.keys(obj); // 得到一個數組
- let index = 0;
- return {
- next: () => {
- if (index < keys.length) {
- // 迭代結果 未結束
- return {
- value: this[keys[index++]],
- done: false
- };
- } else {
- // 迭代結果 結束
- return { value: undefined, done: true };
- }
- }
- };
- }
- }
- for (const value of obj) {
- console.log(value); // value1 value2
- };
for...of 循環內部調用的是數據結構的 Symbol.iterator 方法,for...of 循環可以使用的范圍包括數組、Set 和 Map 結構、某些類似數組的對象(比如 arguments 對象、DOM NodeList 對象)、后文的 Generator 對象,以及字符串。
for...of 循環作為 ES6 新引入的一種循環,具有以下明顯優勢(按需使用):
有著同 for...in 一樣的簡潔語法,但是沒有 for...in 那些缺點(無序,不適用于遍歷數組)。
不同于 forEach 方法,它可以與 break、continue 和 return 配合使用。
提供了遍歷所有數據結構的統一操作接口。
以上是我從 keys()、values()、entries() 遍歷方法出發對遍歷器產生的幾點思考,如有不足之處,歡迎指正~~~