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

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

迭代器與生成器的知識點

1. 迭代器

何爲迭代器?異步

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

2. 生成器

何爲生成器?fetch

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

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 2spa

除了使用函數聲明的方式建立一個生成器外,還可使用函數表達式來建立一個生成器。因爲生成器就是一個函數,一樣可使用對象字面量的方式,將對象的屬性賦值爲一個生成器函數。設計

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

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

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

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爲止,循環就結束。cdn

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

可使用可迭代對象的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. 可以使用生成器構造異步任務處理器;

相關文章
相關標籤/搜索