【ES6基礎】迭代器(iterator)

前端達人發表於2019-06-16

迭代器(iterator)是一個結構化的模式,用於從源以一次一個的方式提取資料。迭代器的使用可以極大地簡化資料操作,於是ES6也向JS中新增了這個迭代器特性。新的陣列方法和新的集合型別(如Set集合與Map集合)都依賴迭代器的實現,這個新特性對於高效的資料處理而言是不可或缺的,在語言的其他特性中也都有迭代器的身影:新的for-of迴圈、展開運算子(...),甚至連非同步程式設計都可以使用迭代器。

今天筆者將從以下幾個方面進行介紹迭代器:

  • 什麼是迭代器(iterator)?
  • 基於協議實現迭代器
  • 迭代器的應用

本篇文章閱讀時間預計6分鐘。

迭代器(iterator)

迭代器是一種有序、連續的、基於拉取的用於消耗資料的組織方式,用於以一次一步的方式控制行為。簡單的來說我們迭代迴圈一個可迭代物件,不是一次返回所有資料,而是呼叫相關方法分次進行返回。

迭代器iterator是一個object,這個object有一個next函式,該函式返回一個有value和done屬性的object,其中value指向迭代序列中當前next函式定義的值。

ES6的迭代協議分為迭代器協議(iterator protocol)和可迭代協議(iterable protocol),迭代器基於這兩個協議進行實現。

迭代器協議: iterator協議定義了產生value序列的一種標準方法。只要實現符合要求的next函式,該物件就是一個迭代器。相當遍歷資料結構元素的指標,類似資料庫中的遊標。

可迭代協議: 一旦支援可迭代協議,意味著該物件可以用for-of來遍歷,可以用來定義或者定製JS 物件的迭代行為。常見的內建型別比如Array & Map都是支援可迭代協議的。物件必須實現@@iterator方法,意味著物件必須有一個帶有@@iterator key的可以通過常量Symbol.iterator訪問到的屬性。

下圖展示了arrays,Maps,Strings資料型別實現了可迭代協議,我們可以使用for-of和展開語法顯示迭代器的資料。

【ES6基礎】迭代器(iterator)

基於協議實現迭代器

迭代器協議

如下程式碼展示了基於迭代協議進行實現:

let obj = {
 array: [1, 2, 3, 4, 5],
 nextIndex: 0,
 next: function() {
         return this.nextIndex < this.array.length ? 
         {value: this.array[this.nextIndex++], done: false} : 
         {done: true}
       }
};
console.log(obj.next().value);
console.log(obj.next().value);
console.log(obj.next().value);
console.log(obj.next().value);
console.log(obj.next().value);
console.log(obj.next().done);複製程式碼

上述程式碼將會輸出

1
2
3
4
5
true複製程式碼

上述程式碼的next方法還可以按如下程式碼進行改寫,看起來更清晰些:

if(this.nextIndex < this.array.length) {
  this.nextIndex++;
  return { value: this.array[this.nextIndex], done: false }
} else {
  return { done: true }
}複製程式碼

我們可以看出,next方法的實現,如果存在新的元素,返回當前元素的並將當前元素位置的標識遞增加1,當沒有元素時,返回{ done: true }。

可迭代協議

根據可迭代協議,物件需要提供@@iterator方法; 也就是說,它必須將Symbol.iterator符號作為屬性鍵。 @@iterator方法必須返回迭代器物件。程式碼實現如下:

let obj = {
  array: [1, 2, 3, 4, 5],
  nextIndex: 0,
  [Symbol.iterator]: function(){
    return {
     array: this.array,
     nextIndex: this.nextIndex,
     next: function(){
       return this.nextIndex < this.array.length ?
          {value: this.array[this.nextIndex++], done: false} :
          {done: true};
     }
    }
  }
};
let iterable = obj[Symbol.iterator]()
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().done);複製程式碼

上述程式碼將會輸出

1
2
3
4
5
true複製程式碼

上述程式碼,我們實現了自定義的迭代器,基於JS的作用域和閉包特性才能輕鬆實現。arrays,Maps,Strings資料型別實現了可迭代協議,其 __proto__原型鏈指向自帶Symbol.iterator的方法,節省了我們手寫程式碼的時間,如下程式碼所示:

const arr = [1, 2];
const iterator = arr[Symbol.iterator](); // returns you an iterator
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())複製程式碼

上述程式碼將會輸出:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }複製程式碼

我們可以使用for-of,展開語法迭代陣列,示例程式碼如下:

const arr = [1, 2];
for(var v of arr){
    console.log(v);
}
//outputs 1
//outputs 2
console.log([...arr]);
//outputs[1,2];複製程式碼

obj物件沒有實現可迭代協議,我們如何迭代obj物件呢?實現obj的迭代器呢,示例程式碼如下:

var obj={
    a:1,
    b:2,
    c:3,
    [Symbol.iterator]:function () {
        var keys=Object.keys(this);//object.vulues(this)
        var index=0;
        return{
            next:()=>
            (index<keys.length)?
            {value: this[keys[index++]], done:false} :
            {done: true,value:undefined}
        }
    }
};
console.log([...obj]);
//outputs [1,2,3]複製程式碼

迭代器應用

斐波那契數列

斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……在數學上,斐波納契數列以如下被以遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)。

我們可以使用迭代器來產生一個序列數不大於100的斐波那契數列,示例程式碼如下:

let Fib= {
    [Symbol.iterator](){
        let n1=1;
        let n2=1;
        let max=100;
        return {
            next(){
                let current=n2;
                n2=n1;
                n1=n1+current;
                if(current<max){
                    return {value: current, done: false}
                }else{
                    return { done: true}
                }

            }
        }
    }
}
console.log([...Fib]);
//outputs [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ]複製程式碼

模擬任務佇列

有時候我們需要要執行任務放在一個佇列裡,分次執行,我們可以使用迭代器進行模擬,示例程式碼如下:

let tasks={
    actions:[],
    [Symbol.iterator](){
        let steps=this.actions.slice();
        return {
            [Symbol.iterator]() {return this;},
            next(...args){
                if(steps.length>0){
                    let res=steps.shift()(...args);
                    return {value:res,done:false};
                }
                else{
                    return {done:true}
                }
            },
            return(v){
                steps.length=0;
                return {value:v,done:true};
            }
        };
    }
};

tasks.actions.push(
    function step1(x) {
        console.log("step 1:",x);
        return x*2;
    },
    function step2(x,y) {
        console.log("step 2:",x,y);
        return x+(y*2);
    },
    function step3(x,y,z) {
        console.log("step 3:",x,y,z);
        return (x*y)+z;
    }
);

let it=tasks[Symbol.iterator]();
console.log(it.next(10));
console.log(it.next(20,50));
console.log(it.next(20,50,120));
console.log(it.next());複製程式碼

上述程式碼輸出:

step 1: 10
{ value: 20, done: false }
step 2: 20 50
{ value: 120, done: false }
step 3: 20 50 120
{ value: 1120, done: false }
{ done: true }複製程式碼

從上述程式碼,我們可以看出,迭代器不僅僅是資料的迭代,還可以作為一個模式來組織相關的功能。(注:本示例來源《你不知道的JavaScript》下卷)

小節

今天的內容就到這裡,迭代器是不是很神奇,好像如魔法一般,我們隨意控制函式的中斷與繼續,豐富了我們解決問題的思路,讓我們的程式碼看起來更加工程化和結構化,提高了程式碼的可讀性和可理解性。


相關文章