ES6系列之一文完全弄懂Iterator

前言

本文主要來深刻剖析ES6的Iterator(迭代器/遍歷器),在瞭解它以前,咱們首先要知道爲何須要Iterator? 它出現的緣由是什麼?javascript

從循環提及

平時開發中,咱們常常會用到循環,拿最基本的來講,好比循環一個數組:html

// for循環
var arr = [1, 2, 3, 4];
for(let i = 0; i < arr.length; i++) {
    console.log(arr[i]); // 1 2 3 4
}

// 也能夠用forEach循環
arr.forEach(item => {
    console.log(item); // 1 2 3 4
})

// 也能夠用for in循環
for(let i in arr) {
    console.log(arr[i]); // 1 2 3 4
}
複製代碼

若是是循環輸出字符串的每個字符java

// for循環
var str = 'abcde';
for(var i = 0; i < str.length; i++) {
    console.log(str[i]); // a b c d e
}

// for in循環
for(var k in str){
    console.log(str[k]); // a b c d e
}

// forEach不能遍歷字符串,若是硬要用則只能先把字符串轉爲數組再用數組方式循環
複製代碼

若是是循環輸出一個map對象呢, 妥妥的就一個foreach循環,普通的for循環和for...in都不行node

var map = new Map();
map.set('first', '第一');
map.set('second', '第二');
map.set('third', '第三');

// forEach循環
map.forEach((val,key) => {
    console.log(val, key);
    // 第一 first
    // 第二 second
    // 第三 third
});
複製代碼

對於上述各集合數據,咱們發現沒有一個循環方法是能夠一次性解決的。數組

雖然forEach循環不能循環字符串,但字符串能夠轉爲數組再使用forEach便可輸出,但這操做並不舒服每次使用都要轉換。並且forEach循環存在缺點:不能使用break,continue語句跳出循環,或者使用return從函數體返回bash

而for循環在有些狀況寫代碼會增長複雜度,並且不能循環對象。數據結構

相比下,for...in的缺點是不只遍歷數字鍵名,還會遍歷手動添加的自定義鍵,甚至包括原型鏈上的鍵。for...in主要仍是爲遍歷對象而設計的,並不太適用於遍歷數組。閉包

以下代碼函數

Array.prototype.protoValue = 'hello';
var arr = [1, 2, 3, 4];
arr.test = 'test';
for(let i in arr) {
    console.log(arr[i]); // 1 2 3 4 test hello
}
複製代碼

話說回來,有沒有一種更好的循環能一統上述循環問題? ES6就有了,用for...of循環ui

for...of循環出現

它的出現解決了什麼問題呢?首先是固然是能解決了上述咱們的問題

對上述數據進行循環輸出

for(let v of arr) {
    console.log(v); // 1 2 3 4
}
for(let v of str) {
    console.log(v); // a b c d e
}
for(let v of map) {
    console.log(v);
    // (2) ["first", "第一"]
    // (2) ["second", "第二"]
    // (2) ["third", "第三"]
}
複製代碼

來看看它的優勢:

  1. 簡潔直接的遍歷數組語法
  2. 它避開了 for-in 循環的全部缺點
  3. 與forEach循環不一樣的是,它可使用break、continue 和 return 語句

阮一峯老師的《ECMAScript 6 入門》提到:

ES6中引入了for...of循環,做爲遍歷全部數據結構的統一的方法。

Iterator是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。

一個數據結構只要具備 iterator 接口,就能夠用for...of循環遍歷它的成員。

也就是說,並非全部的對象都能使用for...of循環,只有實現了Iterator接口的對象,纔可以for...of來進行遍歷取值。

那麼,Iterator是什麼呢?咱們接着說

Iterator迭代器

概念

它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。

做用

  1. 爲各類數據結構,提供一個統一的、簡便的訪問接口;
  2. 使得數據結構的成員可以按某種次序排列;
  3. 創造一種新的遍歷命令for...of循環,Iterator 接口主要供for...of消費。

如今明白了,Iterator的產生主要是爲了使用for...of方法。但具體Iterator概念仍是有些抽象,若是要直接具體的描述的話:

Iterator其實就是一個具備 next()方法的對象,而且每次調用next()方法都會返回一個結果對象,這個結果對象有兩個屬性, 以下

{
    value: 表示當前的值,
    done: 表示遍歷是否結束
}
複製代碼

這裏多了一些概念,咱們來梳理一下:

Iterator是一個特殊的對象:

  1. 它具備next()方法,調用該方法就會返回一個結果對象
  2. 結果對象有兩個屬性值:valuedone
  3. value表示具體的返回值;done是布爾類型,表示集合是否完成遍歷,沒有則返回true,不然返回false
  4. 內部有一個指針,指向數據結構的起始位置。每調用一次next()方法,指針都會向後移動一個位置,直到指向最後一個位置。

模擬Iterator

根據上述描述,咱們來模擬一個迭代器,代碼以下

function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = (i >= item.length);
            var value = !done ? items[i++] : undefined;

            return {
                done: done,
                value: value
            };
        }
    };
}

// iterator 就是一個迭代器對象
var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }
console.log(iterator.next()); // { done: true, value: undefined }
console.log(iterator.next()); // { done: true, value: undefined }
複製代碼

過程:

  1. 建立一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
  2. 第一次調用指針對象的next方法,next 方法內部經過閉包來保存指針i 的值,每次調用i都會+1,指向下一個。故第一次指針指向數據結構的第一個成員,輸出1。
  3. 第二次調用指針對象的next方法,指針就指向數據結構的第二個成員,輸出2。
  4. 第三次調用指針對象的next方法,數組長度爲3,此時數據結構的結束位置,輸出3。
  5. 第四次調用指針對象的next方法,此時已遍歷完成了,輸出done爲true表示完成,value爲undefined,此後第n次調用都是該結果。

看到這裏你們大概知道Iterator了,但Iterator接口主要供for...of消費。咱們試着用for...of循環上面建立Iterator對象

var iterator = makeIterator([1, 2, 3]);

for (let value of iterator) {
    console.log(value); // Uncaught TypeError: iterator is not iterable
}
複製代碼

結果報錯,說明咱們生成的 iterator 對象並非可遍歷的,這樣的結構還不可以被for...of循環

那什麼樣的結構纔是可遍歷的呢?

可迭代對象Iterable

如何實現

ES6還引入了一個新的Symbol對象,symbol值是惟一的。

一個數據結構只要部署了Symbol.iterator屬性,就被視爲具備 iterator 接口;調用這個接口,就會返回一個遍歷器對象。這樣的數據結構才能被稱爲可迭代對象(Iterator),該對象可被for...of遍歷。

照着上面的規定來建立一個可迭代對象

var arr = [1, 2, 3];

arr[Symbol.iterator] = function() {
    var _this = this;
    var i = 0;
    return {
        next: function() {
            var done = (i >= _this.length);
            var value = !done ? _this[i++] : undefined;
            return {
                done: done,
                value: value
            };
        }
    };
}

// 此時能夠for...of遍歷
for(var item of arr){
    console.log(item); // 1 2 3 
}
複製代碼

由此,咱們也能夠知道 for...of 遍歷的實際上是對象的 Symbol.iterator 屬性。

原生具有Iterator接口的數據結構

在ES6中,全部的集合對象,包括數組,類數組對象(arguments對象、DOM NodeList 對象),Map和Set,還有字符串都是可迭代的,均可以被for...of遍歷,由於他們都有默認的迭代器。

下面就挑其中幾個類型來舉例子:

數組

var arr = [1, 2, 3];
 
var iteratorObj = arr[Symbol.iterator]();

console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());
複製代碼

輸出結果:

由上述對迭代器的概念認識,輸出結果也徹底符合咱們預期。

arguments

咱們知道普通對象是默認沒有部署這個接口的,因此arguments這個屬性沒有在原型上,而是在對象自身的屬性上。

function test(){
   var obj = arguments[Symbol.iterator]();
   console.log(arguments);
   console.log(obj.next());
   console.log(obj.next());
   console.log(obj.next());
}

test(1, 2, 3);
複製代碼

輸出結果:

NodeList

<div class="test">1</div>
<div class="test">2</div>
<div class="test">3</div>
複製代碼
const nodeList = document.getElementsByClassName('test')
for (const node of nodeList) {
    console.log(node);
}
複製代碼

輸出結果:

map

此次直接從原型上找來證實

console.log(Map.prototype.hasOwnProperty(Symbol.iterator)); // true
複製代碼

至於其餘類型的可迭代對象你們可觸類旁通。

調用 Iterator 接口的場合

有一些場合會默認調用 Iterator 接口(即Symbol.iterator方法)

解構賦值

let set = new Set().add('a').add('b').add('c');

let [x, y] = set;
console.log(x, y); // a b
複製代碼

擴展運算符

擴展運算符(...)也會調用默認的 Iterator 接口,能夠將當前迭代對象轉換爲數組。

var str = 'hello';
console.log([...str]); // ["h", "e", "l", "l", "o"]

let arr = ['b', 'c'];
console.log(['a', ...arr, 'd']); // ["a", "b", "c", "d"]
複製代碼

yield*

yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。

let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

var iterator = generator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
複製代碼

關於yield以後會再寫一篇專題

其餘場合

例如:for...of、Set()、Map()、Array.from()等。咱們主要是爲了證實調用上面這些函數時着實是用到了Iterator接口。

咱們貼出上面的實現可迭代對象的代碼,進行改動。想想,儘管咱們沒有手動添加 Symbol.iterator屬性,但由於 ES6 默認部署了 Symbol.iterator,數組仍是能夠遍歷成功,那固然咱們也能夠手動修改這個屬性,從新部署也能夠。

若是咱們手動修改生效,影響了輸出,證實某方法調用須要用到Iterator接口

var arr = [1, 2, 3];

arr[Symbol.iterator] = function() {
    var _this = this;
    var i = 0;
    return {
        next: function() {
            var done = (i >= _this.length);
            var value = !done ? _this[i++] : undefined;
            return {
                done: done,
                value: value + ' 手動添加的屬性' // 添加自定義值
            };
        }
    };
}

// 輸出結果顯示手動修改生效,證實for...of調用了Iterator接口
for(var item of arr){
    console.log(item);
    // 1 手動添加的屬性
    // 2 手動添加的屬性
    // 3 手動添加的屬性
}

var set = new Set(arr);
console.log(set); // Set(3) {"1 手動添加的屬性", "2 手動添加的屬性", "3 手動添加的屬性"}
複製代碼

上述其餘方法證實也是如此,就不一一列出了。

計算生成的數據結構

有些數據結構是在現有數據結構的基礎上,計算生成的。好比,ES6 的數組、Set、Map 都部署瞭如下三個方法,調用後都返回遍歷器對象。

  1. entries() 返回一個遍歷器對象,用來遍歷[鍵名,鍵值]組成的數組。對於數組,鍵名就是索引值。
  2. keys() 返回一個遍歷器對象,用來遍歷全部的鍵名。
  3. values() 返回一個遍歷器對象,用來遍歷全部的鍵值。
var arr = ["first", "second", "third"];

for (let index of arr.keys()) {
    console.log(index);
}

// 0
// 1
// 2

for (let color of arr.values()) {
    console.log(color);
}

// first
// second
// third

for (let item of arr.entries()) {
    console.log(item);
}

// [ 0, "first" ]
// [ 1, "second" ]
// [ 2, "third" ]
複製代碼

判斷對象是否可迭代

const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
console.log(isIterable(new Set())); // true 
console.log(isIterable(new Map())); // true 
console.log(isIterable([1, 2, 3])); // true 
console.log(isIterable("hello world")); // true 
複製代碼

for...of循環不支持遍歷普通對象

梳理了半天,回到最初,Iterator的產生主要是爲了使用for...of方法。而對象不像數組的值是有序的,遍歷的時候根本不知道如何肯定他們的前後順序,因此要注意的是for...of循環不支持遍歷對象。

若是非要遍歷對象,同理對象也必須包含[Symbol.iterator]的屬性並實現迭代器方法,能夠經過手動利用Object.defineProperty方法添加該屬性。

var obj = { a: 2, b: 3 }
for (let i of obj) {
    console.log(i) // Uncaught TypeError: obj is not iterable
}
複製代碼

關於Iterator就寫到這裏了,下一篇寫ES6的Generator生成器。

若是該文對你有幫助,點個贊哦,若有錯誤請指正~~~

相關文章
相關標籤/搜索