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

你聽___發表於2018-05-01

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

主要知識點:迭代器、生成器、可迭代物件以及for-of迴圈、迭代器的高階功能以及建立非同步任務處理器

迭代器與生成器的知識點

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. 能夠使用生成器構造非同步任務處理器;

相關文章