原創文章&經驗總結&從校招到A廠一路陽光一路滄桑
詳情請戳www.codercc.com
主要知識點:迭代器、生成器、可迭代對象以及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.iterato
r屬性來檢查對象是否是可迭代對象。
創建可迭代對象
數組,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():返回一個包含集合中的鍵的迭代器;
- 調用entries()迭代器會在每次調用next()方法返回一個雙項數組,此數組代表集合數據項中的鍵和值:對于數組來說,第一項是數組索引;對于Set來說,第一項是值(因為Set的鍵和值相同),對于Map來說,就是鍵值對的值;
- values()迭代器能夠返回集合中的每一個值;
- 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);
}
});
上面的示例代碼就是一個簡單的異步任務處理器,有這樣幾點要點:
- 使用生成器構造迭代器,所以在
run
方法中傳入的是生成器函數; - 生成器函數中
yield
關鍵字,返回的是一個能夠執行回調函數的函數,并且回調函數是該函數的一個參數;
7. 總結
使用迭代器可以用來遍歷集合對象包含的數據,調用迭代器的
next()
方法可以返回一個結果對象,其中value
屬性代表值,done
屬性用來表示集合對象是否已經到了最后一項,如果集合對象的值全部遍歷完后,done
屬性為true
;Symbol.iterator
屬性被用于定義對象的默認迭代器,使用該屬性可以為對象自定義迭代器。當Symbol.iterator
屬性存在時,該對象可以被認為是可迭代對象;可迭代對象可以使用for-of循環,for-of循環不需要關注集合對象的索引,更能專注于對內容的處理;
數組、Set、Map以及字符串都具有默認的迭代器;
擴展運算符可以作用于任何可迭代對象,讓可迭代對象轉換成數組,并且擴展運算符可以用于數組字面量中任何位置中,讓可迭代對象的數據項一次填入到新數組中;
生成器是一個特殊的函數,語法上使用了
*
,yield能夠返回結果,并能暫停繼續往下執行,直到調用next()方法后,才能繼續往下執行。使用生成器委托能夠將兩個生成器合并組合成一個生成器;能夠使用生成器構造異步任務處理器;