深入理解ES6--8.迭代器與生成器

原創文章&經驗總結&從校招到A廠一路陽光一路滄桑

詳情請戳www.codercc.com

image

主要知識點:迭代器、生成器、可迭代對象以及for-of循環、迭代器的高級功能以及創建異步任務處理器

迭代器與生成器.png

1. 迭代器

何為迭代器?

迭代器是被設計專用于迭代的對象,帶有特定接口。所有的迭代器對象都擁有 next() 方
法,會返回一個結果對象。該結果對象有兩個屬性:對應下一個值的 value ,以及一個布爾
類型的 done ,其值為 true 時表示沒有更多值可供使用。迭代器持有一個指向集合位置的
內部指針,每當調用了 next() 方法,迭代器就會返回相應的下一個值。

2. 生成器

何為生成器?

生成器(generator ) 是能返回一個迭代器的函數。生成器函數由放在 function 關鍵字之后的一個星號( * ) 來表示,并能使用新的 yield 關鍵字。將星號緊跟在 function 關鍵字之后,或是在中間留出空格,都是沒問題的。例如:

function*generator(){

    yield 1;
    yield 2;
    yield 3;
}

let iterator = generator();
console.log(iterator.next().value);//1
console.log(iterator.next().value);//2

生成器函數最有意思的地方是它們會在每一個yield語句后停止,例如在上面的代碼中執行yield 1后,該函數不會在繼續往下執行。等待下一次調用next()后,才會繼續往下執行yield 2

除了使用函數聲明的方式創建一個生成器外,還可以使用函數表達式來創建一個生成器。由于生成器就是一個函數,同樣可以使用對象字面量的方式,將對象的屬性賦值為一個生成器函數。

3. 可迭代對象與for-of循環

可迭代對象是包含Symbol.iterator屬性的對象,這個Symbol.iterator屬性對應著能夠返回該對象的迭代器的函數。在ES6中,所有的集合對象(數組、Set和Map)以及字符串都是可迭代對象,因此它們都被指定了默認的迭代器。可迭代對象可以與ES6中新增的for-of循環配合使用。

迭代器解決了for循環中追蹤索引的問題,而for-of循環,則是完全刪除追蹤集合索引的需要,更能專注于操作集合內容。for-of循環在循環每次執行時會調用可迭代對象的next()方法,并將結果對象的value值存儲在一個變量上,循環過程直到結果對象done屬性變成true為止:

let arr = [1,2,3];
for(let num of arr){
    console.log(num);
}
輸出結果為:1,2,3

for-of循環首先會調用arr數組中Symbol.iterator屬性對象的函數,就會獲取到該數組對應的迭代器,接下來iterator.next()被調用,迭代器結果對象的value屬性會被放入到變量num中。數組中的數據項會依次存入到變量num中,直到迭代器結果對象中的done屬性變成true為止,循環就結束。

訪問可迭代對象的默認迭代器

可以使用可迭代對象的Symbol.iterator來訪問對象上可返回迭代器的函數:

let arr = [1,2,3];
//訪問默認迭代器
let iterator = arr[Symbol.iterator]();
console.log(iterator.next().value); //1
console.log(iterator.next().value); //2

通過Symbol.iterator屬性獲取到該對象的可返回迭代器的函數,然后執行該函數得到對象的可迭代器。同樣的,可是使用Symbol.iterator屬性來檢查對象是否是可迭代對象。

創建可迭代對象

數組,Set等集合對象是默認的迭代器,當然也可以為對象創建自定義的迭代器,使其成為可迭代對象。那么迭代器如何生成?我們已經知道,生成器就是一個可以返回迭代器的函數,因此自定義迭代器,就是寫一個生成器函數。同時,可迭代對象必須具有Symbol.iterator屬性,并且該屬性對應著一個能夠返回迭代器的函數,因此只需要將這個生成器函數賦值給Symbol.iterator屬性即可:

//創建可迭代對象

let obj = {
    items:[],

    *[Symbol.iterator](){
        for(let item of this.items){
            yield item;
        }
    }
}

obj.items.push(1);
obj.items.push(2);

for(let num of obj){
    console.log(num);
}

輸出:1,2

4. 內置的迭代器

ES6中許多內置類型已經包含了默認的迭代器,只有當默認迭代器滿足不了時,才會創建自定義的迭代器。如果新建對象時,要想把該對象轉換成可迭代對象的話,一般才會需要自定義迭代器。

集合迭代器

ES6中有三種集合對象:數組、Map和Set,這三種類型都擁有默認的迭代器:

  • entries():返回一個包含鍵值對的迭代器;
  • values():返回一個包含集合中的值的迭代器;
  • keys():返回一個包含集合中的鍵的迭代器;
  1. 調用entries()迭代器會在每次調用next()方法返回一個雙項數組,此數組代表集合數據項中的鍵和值:對于數組來說,第一項是數組索引;對于Set來說,第一項是值(因為Set的鍵和值相同),對于Map來說,就是鍵值對的值;
  2. values()迭代器能夠返回集合中的每一個值;
  3. keys()迭代器能夠返回集合中的每一個鍵;

集合的默認迭代器

當for-of循環沒有顯式指定迭代器時,集合對象會有默認的迭代器。values()方法是數組和Set默認的迭代器,而entries()方法是Map默認迭代器。

字符串的迭代器

ES6旨在為Unicode提供了完全支持,字符串的默認迭代器就是解決字符串迭代問題的一種嘗試,這樣一來,借助字符串默認迭代器就能處理字符而非碼元:

//字符串默認迭代器
let str ='A   B';
for(let s of str){
    console.log(s); //A  B
}

擴展運算符與非數組的可迭代對象

擴展運算符能作用于所有可迭代對象,并且會使用默認迭代器來判斷需要哪些值。在數組字面量中可以使用擴展運算符將可迭代對象填充到數組中:

//擴展運算符可作用到所有可迭代對象
let arr = [1,2,3];
let array = [...arr];
console.log(array); [1,2,3]

并且,可以不限次數在數組字面量中使用擴展運算符,而且可以在任意位置用擴展運算符將可迭代對象填充到數組中:

let arr = [1,2,3];
let arr2 = [7,8,9];
let array = [...arr,5,...arr2];
console.log(array); //1,2,3,5,7,8,9

5. 迭代器高級功能

能夠通過next()方法向迭代器傳遞參數,當一個參數傳遞給next()方法時,該參數就會成為生成器內部yield語句中的變量值。

//迭代器的高級功能
function * generator(){
    let first = yield 1;
    let second = yield first+2;
    let third = yield second+3;
}

let iterator = generator();
console.log(iterator.next()); //{value: 1, done: false}
console.log(iterator.next(4)); //{value: 6, done: false}
console.log(iterator.next(5)); //{value: 8, done: false}
console.log(iterator.next()); //{value: undefined, done: true}

示例代碼中,當通過next()方法傳入參數時,會賦值給yield語句中的變量。

在迭代器中拋出錯誤

能傳遞給迭代器的不僅是數據,還可以是錯誤,迭代器可以選擇一個throw()方法,用于指示迭代器應在恢復執行時拋出一個錯誤:

//迭代器拋出錯誤

function * generator(){
    let first = yield 1;        
    let second = yield first+2;     
    let third = yield second+3;
}


let iterator = generator();
console.log(iterator.next()); //{value: 1, done: false}
console.log(iterator.next(4)); //{value: 6, done: false}
console.log(iterator.throw(new Error('Error!'))); //Uncaught Error: Error!
console.log(iterator.next()); //不會執行

在生成器中同樣可以使用try-catch來捕捉錯誤:

function * generator(){
    let first = yield 1;
    let second;
    try{
        second = yield first+2;
    }catch(ex){
        second = 6
    }   
    let third = yield second+3;
}


let iterator = generator();
console.log(iterator.next()); //{value: 1, done: false}
console.log(iterator.next(4)); //{value: 6, done: false}
console.log(iterator.throw(new Error('Error!'))); //{value: 9, done: false}
console.log(iterator.next()); //{value: undefined, done: true}

生成器的return語句

由于生成器是函數,你可以在它內部使用 return 語句,既可以讓生成器早一點退出執行,也可以指定在 next() 方法最后一次調用時的返回值。大多數情況,迭代器上的
next() 的最后一次調用都返回了 undefined ,但你還可以像在其他函數中那樣,使用
return 來指定另一個返回值。在生成器內, return 表明所有的處理已完成,因此 done
屬性會被設為 true ,而如果提供了返回值,就會被用于 value 字段。比如,利用return讓生成器更早的退出:

function * gene(){
    yield 1;
    return;
    yield 2;
    yield 3;
}

let iterator = gene();
console.log(iterator.next());//{value: 1, done: false}
console.log(iterator.next());//{value: undefined, done: true}
console.log(iterator.next());//{value: undefined, done: true}

由于使用return語句,能夠讓生成器更早結束,因此在第二次以及第三次調用next()方法時,返回結果對象為:{value: undefined, done: true}

還可以使用return語句指定最后返回值:

function * gene(){
    yield 1;
    return 'finish';
}

let iterator = gene();
console.log(iterator.next());//{value: 1, done: false}
console.log(iterator.next());//{value: "finish", done: true}
console.log(iterator.next());//{value: undefined, done: true}

當第二次調用next()方法時,返回了設置的返回值:finish。第三次調用 next() 返回了一個對象,其 value 屬性再次變回undefined ,你在 return 語句中指定的任意值都只會在結果對象中出現一次,此后 value 字段就會被重置為 undefined

生成器委托

生成器委托是指:將生成器組合起來使用,構成一個生成器。組合生成器的語法需要yield**落在yield關鍵字與生成器函數名之間即可:

function * gene1(){
    yield 'red';
    yield 'green';

}
function * gene2(){
    yield 1;
    yield 2;
}

function * combined(){
    yield * gene1();
    yield * gene2();
}

let iterator = combined();
console.log(iterator.next());//{value: "red", done: false}
console.log(iterator.next());//{value: "green", done: false}
console.log(iterator.next());//{value: 1, done: false}
console.log(iterator.next());//{value: 2, done: true}
console.log(iterator.next());//{value: undefined, done: true}

此例中將生成器gene1和gene2組合而成生成器combined,每次調用combined的next()方法時,實際上會委托到具體的生成器中,當gene1生成器中所有的yield執行完退出之后,才會繼續執行gene2,當gene2執行完退出之后,也就意味著combined生成器執行結束。

在使用生成器委托組合新的生成器時,前一個執行的生成器返回值可以作為下一個生成器的參數:

//利用生成器返回值

function * gene1(){
    yield 1;
    return 2;
}

function * gene2(count){

    for(let i=0;i<count;i++){
        yield 'repeat';
    }
}

function * combined(){
    let result = yield * gene1();
    yield result;
    yield*gene2(result);
}
let iterator = combined();
console.log(iterator.next());//{value: 1, done: false}
console.log(iterator.next());//{value: 2, done: false}
console.log(iterator.next());//{value: "repeat", done: false}
console.log(iterator.next());//{value: "repeat", done: false}
console.log(iterator.next());//{value: undefined, done: true}

此例中,生成器gene1的返回值,就作為了生成器gene2的參數。

6. 異步任務

一個簡單的任務運行器

生成器函數中yield能暫停運行,當再次調用next()方法時才會重新往下運行。一個簡單的任務執行器,就需要傳入一個生成器函數,然后每一次調用next()方法就會“一步步”往下執行函數:

//任務執行器
function run(taskDef) {
    // 創建迭代器,讓它在別處可用
    let task = taskDef();
    // 啟動任務
    let result = task.next();
    // 遞歸使用函數來保持對 next() 的調用
    function step() {
    // 如果還有更多要做的
    if (!result.done) {
        result = task.next();
        step();
    }
    } 
    // 開始處理過程
    step();
}


run(function*() {
    console.log(1);
    yield;
    console.log(2);
    yield;
    console.log(3);
});

run() 函數接受一個任務定義(即一個生成器函數) 作為參數,它會調用生成器來創建一個
迭代器,并將迭代器存放在 task 變量上。第一次對 next() 的調用啟動
了迭代器,并將結果存儲下來以便稍后使用。step() 函數查看result.done 是否為 false,如果是就在遞歸調用自身之前調用 next() 方法。每次調用 next() 都會把返回的結果保
存在 result 變量上,它總是會被最新的信息所重寫。對于 step() 的初始調用啟動了處理
過程,該過程會查看 result.done 來判斷是否還有更多要做的工作。

能夠傳遞數據的任務運行器

如果需要傳遞數據的話,也很容易,也就是將上一次yield的值,傳遞給下一次next()調用即可,僅僅只需要傳送結果對象的value屬性:

//任務執行器
function run(taskDef) {
    // 創建迭代器,讓它在別處可用
    let task = taskDef();
    // 啟動任務
    let result = task.next();
    // 遞歸使用函數來保持對 next() 的調用
    function step() {
        // 如果還有更多要做的
        if (!result.done) {
            result = task.next(result.value);               
            console.log(result.value); //6 undefined
            step();
        }
    } 
    // 開始處理過程
    step();
}


run(function*() {
    let value = yield 1;
    yield value+5;
});

異步任務

上面的例子是簡單的任務處理器,甚至還是同步的。實現任務器也主要是迭代器在每一次調用next()方法時彼此間傳遞靜態參數。如果要將上面的任務處理器改裝成異步任務處理器的話,就需要yield能夠返回一個能夠執行回調函數的函數,并且回調參數為該函數的參數即可。

什么是有回調函數的函數?

有這樣的示例代碼:

function fetchData(callback) {
    return function(callback) {
        callback(null, "Hi!");
    };
}

函數fetchData返回的是一個函數,并且所返回的函數能夠接受一個函數callback。當執行返回的函數時,實際上是調用回調函數callback。但目前而言,回調函數callback還是同步的,可以改造成異步函數:

function fetchData(callback) {
    return function(callback) {
            setTimeout(function() {
                callback(null, "Hi!");
        }, 50);
    };
}

一個簡單的異步任務處理器:

//異步任務處理器

function run(taskDef){

    //執行生成器,創建迭代器
    let task = taskDef();
    //啟動任務
    let result = task.next();

    function step(){
        while(!result.done){
            if(typeof(result.value)==='function' ){
                result.value(()=>{
                    console.log('hello world');
                })
            }                   
            result = task.next();
            step();
        }           
    }
    step();
}




run(function *(){
    //返回一個能夠返回執行回調函數的函數,并且回調函數還是該
    //函數的參數
    yield function(callback){
        setTimeout(callback,3000);
    }
});

上面的示例代碼就是一個簡單的異步任務處理器,有這樣幾點要點:

  1. 使用生成器構造迭代器,所以在run方法中傳入的是生成器函數;
  2. 生成器函數中yield關鍵字,返回的是一個能夠執行回調函數的函數,并且回調函數是該函數的一個參數

7. 總結

  1. 使用迭代器可以用來遍歷集合對象包含的數據,調用迭代器的next()方法可以返回一個結果對象,其中value屬性代表值,done屬性用來表示集合對象是否已經到了最后一項,如果集合對象的值全部遍歷完后,done屬性為true

  2. Symbol.iterator屬性被用于定義對象的默認迭代器,使用該屬性可以為對象自定義迭代器。當Symbol.iterator屬性存在時,該對象可以被認為是可迭代對象;

  3. 可迭代對象可以使用for-of循環,for-of循環不需要關注集合對象的索引,更能專注于對內容的處理;

  4. 數組、Set、Map以及字符串都具有默認的迭代器;

  5. 擴展運算符可以作用于任何可迭代對象,讓可迭代對象轉換成數組,并且擴展運算符可以用于數組字面量中任何位置中,讓可迭代對象的數據項一次填入到新數組中;

  6. 生成器是一個特殊的函數,語法上使用了*,yield能夠返回結果,并能暫停繼續往下執行,直到調用next()方法后,才能繼續往下執行。使用生成器委托能夠將兩個生成器合并組合成一個生成器;

  7. 能夠使用生成器構造異步任務處理器;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容