for…of使用
- 2020 年 8 月 6 日
- 筆記
3、for…of使用
3.1 for…of使用
for…of 一種用於遍曆數據結構的方法。它可遍歷的對象包括數組,對象,字符串,set和map結構等具有iterator 接口的數據結構。
我們先來看看幾種傳統的遍曆數組的方式以及它們的缺陷:
方式一:
方式一利用for循環來遍曆數組的缺點就是:代碼不夠簡潔。
方式二:
利用forEach循環代碼量少了很多,寫法更加簡潔,缺點就是:無法中斷停止整個循環。
方式三:
for…in循環更常用於對象的循環,如果用於數組的循環,那麼就要注意了,上述代碼中每次循環中得到的i是字符串類型,而不是預料中的數字類型,要想對它進行運算,那得先要進行類型轉換,造成不方便。
那我們來看看for…of的是實現:
for…of的優勢:
- 寫法比for循環簡潔很多;
- 可以用break來終止整個循環,或者continute來跳出當前循環,繼續後面的循環;
結合keys( )獲取到循環的索引,並且是數字類型,而不是字符串類型。
3.2 循環可以終止
以上案例:用break實現了終止整個循環,不會繼續後面的遍歷,所以打印結果為:1 2。
可以跳過當前循環:
用continue跳過當前循環,繼續後面的循環,所以打印結果為:1 2 4 5。
3.3得到數字類型索引
使用數組的擴展keys( ),獲取鍵名再遍歷,得到的index是數字類型的。
此外,相比於for…in循環專門為對象設計,for…of循環的適用範圍更廣。
3.4遍歷字符串
3.5 遍歷DOM list
for…of支持類數組的遍歷,例如DOM List。
for…of支持set和map解構的遍歷。
4 、Iterator遍歷器
4.1 for…of為什麼不遍歷object對象
我們講了簡單又實用的for…of,我們可以使用它來遍曆數組,字符串,Set和Map結構,但是有沒有發現,我們並沒有說它可以遍歷Object對象,為什麼不試試用它來遍歷Object對象呢?
//定義一個的Object對象
let obj = {"name":"前端君"};
//咱們來for...of一下
for(let v of obj){
console.log(v);
}
//結果:報錯
//錯誤提示:obj[Symbol.iterator]不是一個function
oh no,程序報錯了,for…of根本不支持遍歷普通的Object對象,還出現了錯誤提示:obj[Symbol.iterator]不是一個function,這是什麼鬼?
為什麼數組,Set和Map結構又可以支持for…of的遍歷呢?
原來,要想能夠被for…of正常遍歷的,都需要實現一個遍歷器Iterator。而數組,Set和Map結構,早就內置好了遍歷器Iterator(又叫迭代器),它們的原型中都有一個Symbol.iterator方法;而Object對象並沒有實現這個接口,使得它無法被for…of遍歷。
那麼,我們就親自來驗證一下,它們的原型中到底是不是有個叫Symbol.iterator的方法:
//數組
Array.prototype[Symbol.iterator];
//結果:function values(){...}
//字符串
String.prototype[Symbol.iterator];
//結果:function [Symbol.iterator](){...}
//Set結構
Set.prototype[Symbol.iterator];
//結果:function values(){...}
//Map結構
Map.prototype[Symbol.iterator];
//結果:function entries(){...}
//Object對象
Object.prototype[Symbol.iterator];
//結果:undefined
從上往下看,確實,唯獨Object對象的原型上沒有Symbol.iterator,返回了:undefined。其他的數據類型的原型上都含有一個名字叫Symbol.iterator的方法Function。
注意:Symbol.iterator 是Symbol 對象的 iterator 屬性,是一個特殊的Symbol值,因此,當它作為prototype對象屬性名的時候,獲取它的時候需要使用[ ]的形式: prototype[Symbol.iterator],不能使用點形式獲取:prototype.Symbol.iterator。
也就說,只要一個數據結構擁有一個叫[Symbol.iterator]()方法的數據結構,就可以被for…of遍歷,我們稱之為:可遍歷對象。比如:數組,字符串,Set和Map結構。
現在你該知道為什麼文章第一個案例會出現錯誤提示:obj[Symbol.iterator]不是一個function 了吧,因為Object對象的原型上壓根就沒有[Symbol.iterator]() 方法啊。
4.2Iterator原理
當可遍歷對象被for…of遍歷的時候,[Symbol.iterator]()就會被調用,返回一個iterator對象。其中還有一個很重要的方法:next( );
//數組:一個可遍歷對象
let arr = ['a','b','c'];
//調用數組的Symbol.iterator()方法
let iter = arr[Symbol.iterator]();
iter.next();
//結果:{value: "a", done: false}
iter.next();
//結果:{value: "b", done: false}
iter.next();
//結果:{value: "c", done: false}
iter.next();
//結果:{value: undefined, done: true}
第1次調用next( )方法:返回數組的第1個元素:「a」,以及done的值為fasle,表示循環沒有結束,繼續遍歷。
第2次調用next( )方法:返回數組的第2個元素:「b」,以及done的值還是為fasle,表示循環沒有結束,繼續遍歷。
第3次調用next( )方法:返回數組的第3個元素:「c」,以及done的值依然為fasle,表示循環沒有結束,繼續遍歷。
第4次調用next( )方法:返回的value值為undefined,以及done的值變成了true,表示遍歷結束。
原來,for…of的原理就是:先調用可遍歷對象的[Symbol.iterator]( )方法,得到一個iterator遍歷器對象,然後就在遍歷器上不斷調用next( )方法,直到done的值為true的時候,就表示遍歷完成結束了。
4.3 自定義Iterator遍歷器
既然有了[Symbol.iterator]()方法就算是可遍歷對象,那麼我給Object對象手動加上一個[Symbol.iterator]()方法,那麼它是不是可以被for…of遍歷了?
那我們就試試看,給一個Object對象加一個[Symbol.iterator]( )方法,看它是不是就能被for…of遍歷了?
1 //定義一個的Object對象 2 let obj = { 3 0:"我是0", 4 1:"我是1", 5 2:"我是2", 6 length:3, 7 //添加[Symbol.iterator]方法 8 [Symbol.iterator] : function() { 9 let _this = this; 10 let index = 0; 11 return { 12 next:() => { 13 let value = _this[index]; 14 let done = (index >= _this.length); 15 index++; 16 return {value,done} 17 } 18 } 19 } 20 }; 21 22 //咱們來for...of一下 23 for(let v of obj){ 24 console.log(v); 25 } 26 //結果:"我是0" 27 // "我是1" 28 // "我是2"
上面這個案例也許你看了覺得很複雜,沒看懂,沒關係,我一起來分析它的結構就夠了!
我們定義了一個Object對象,同時給它添加了[Symbol.iterator]()方法,並在[Symbol.iterator]()方法實現了next( )方法,next( )方法返回的對象包含了value屬性和done屬性。
具體細節如果不理解沒關係,我們確實看到了給Object對象加上了[Symbol.iterator]()方法後,最後確實能被for…of遍歷了。
這就是說,我們可以創建一個可遍歷的對象,並且自定義它的遍歷行為。或者說可以通過添加[Symbol.iterator]()方法,把一個不可遍歷的Object對象,變成可遍歷的對象。
4.4Iterator遍歷器的價值
新特性for…of之所以能夠遍歷各種不同的數據結構,正是因為這個數據結構都實現了Iterator遍歷器接口,供for…of遍歷。如果沒有實現Iterator接口,則該數據結構無法被for…of遍歷,比如:普通的Object對象。
5 、Generator函數
5.1 聲明Generator函數
Generator函數,又稱生成器函數,是ES6的一個重要的新特性。
1 //聲明一個Hello的Generator函數 2 function* Hello(name) { 3 yield `hello ${name}`; 4 yield `how are you`; 5 yield `bye`; 6 }
上面這個就是Generator函數,乍一看,是不是跟普通的函數沒什麼兩樣?確實很像,但是我們要知道它有兩個重要的區別:
- 普通函數用function來聲明,Generator函數用function*聲明。
Generator函數函數內部有新的關鍵字:yield( 產出),普通函數沒有。
5.12調用Generator函數
帶着這兩個疑問我們往下看,我們試着就調用一下這個名字叫Hello的Generator函數,看看會發生什麼:
1 //聲明一個Hello的Generator函數 2 function* Hello(name) { 3 yield `hello ${name}`; 4 yield `how are you`; 5 yield `bye`; 6 } 7 8 //調用Hello函數 9 let ite = Hello('前端君'); 10 //結果:[object Generator] 11 12 ite.next(); 13 //{value: "hello 前端君", done: false} 14 15 ite.next(); 16 //{value: "how are you", done: false} 17 18 ite.next(); 19 //{value: "bye", done: false} 20 21 ite.next(); 22 //{value: undefined, done: true}
看到這裡,估計你也看到了一個熟悉的面孔:next()方法。一開始,我們調用Hello(「前端君」),函數執行後,返回了一個:[object Genrator]生成器對象,我們把它賦值到變量ite中,僅此而已,並沒有做太多的事情。
接着,第1次調用生成器對象ite的next( )方法,返回了一個對象:
1 {value: "hello 前端君", done: false}
第2次調用生成器對象ite的next( )方法,同樣得到了一個對象:
1 {value: "how are you", done: false}
第3次調用生成器對象ite的next( )方法,又得到了一個對象:
1 {value: "bye", done: false}
直到,第4次調用生成器對象ite的next( )方法,返回的對象:
1 {value: undefined, done: true}
看到這裡有沒有發現,這裡生成器的next( )方法的和遍歷器iterator的next( )方法的返回結果是不是一樣?
沒錯,你可以把Generator函數被調用後得到的生成器理解成一個遍歷器iterator,用於遍歷函數內部的狀態。
5.3 Generator函數的行為
通過上面的案例,我們知道了:Generator函數被調用後並不會一直執行到最後,它是先回返回一個生成器對象,然後hold住不動,等到生成器對象的next( )方法被調用後,函數才會繼續執行,直到遇到關鍵字yield後,又會停止執行,並返回一個Object對象,然後繼續等待,直到next( )再一次被調用的時候,才會繼續接着往下執行,直到done的值為true。
5.4 yield語句的使用
而yield在這裡起到了十分重要的作用,就相當於暫停執行並且返回信息。有點像傳統函數的return的作用,但不同的是普通函數只能return一次,但是Generator函數可以有很多個yield。而return代表的是終止執行,yield代表的是暫停執行,後續通過調用生成器的next( )方法,可以恢復執行。
5.5 next方法接收參數
此外,next( )方法還可以接受一個參數,它的參數會作為上一個yield的返回值,我們來看一下:
1 //聲明一個Hello的Generator函數 2 function* Hello() { 3 let res = yield `hello`; 4 yield res; 5 } 6 7 let iterator = Hello(); 8 //結果:一個生成器對象 9 10 iterator.next(); 11 //結果:{value: "hello", done: false} 12 13 iterator.next("前端君"); 14 //結果:{value: "前端君", done: false}
注意函數體內的第一個yield關鍵字,我們把它的返回值賦值給了一個變量res。
再看2次next方法的調用:
第1次調用next( )方法,返回的對象屬性value值為「hello」,屬性done值為:fasle,並暫停執行。
第2次next( )方法,傳入參數:字符串「前端君」。此時,第二個yield關鍵字緊跟着的是變量res,而變量res的值正是上一個關鍵字yield的返回值。也就是說這個值正是我們傳入的參數:「前端君」。因為:next( )的參數會作為上一個yield的返回值。
5.6 關鍵字『yield*』
在一個Generator函數裏面,如果我們想調用另一個Generator函數,就需要用到的關鍵字是:yield*。
我們來看看怎麼玩,代碼有點長,但是很好理解:
1 //聲明Generator函數:gen1 2 function* gen1() { 3 yield "gen1 start"; 4 yield "gen1 end"; 5 } 6 7 //聲明Generator函數:gen2 8 function* gen2() { 9 yield "gen2 start"; 10 yield "gen2 end"; 11 } 12 13 //聲明Generator函數:start 14 function* start() { 15 yield "start"; 16 yield* gen1(); 17 yield* gen2(); 18 yield "end"; 19 } 20 21 //調用start函數 22 var ite = start(); 23 //創建一個生成器 24 25 ite.next(); 26 //{value: "start", done: false} 27 28 ite.next(); 29 //{value: "gen1 start", done: false} 30 31 ite.next(); 32 //{value: "gen1 end", done: false} 33 34 ite.next(); 35 //{value: "gen2 start", done: false} 36 37 ite.next(); 38 //{value: "gen2 end", done: false} 39 40 ite.next(); 41 //{value: "end", done: false} 42
我們主要看start( )這個Generator函數,其中有兩句代碼:
1 yield* gen1(); 2 yield* gen2();
這裡使用了關鍵字yield*來實現調用另外兩個Generator函數。從後面的多個next( )方法得到的結果看,我們可以知道:
如果一個Generator函數A執行過程中,進入(調用)了另一個Generator函數B,那麼會一直等到Generator函數B全部執行完畢後,才會返回Generator函數A繼續執行。
5.7 Generator函數的用途
以上就是對Generator函數的講解介紹,它是ES6的一個很重要的新特性。它可以控制函數的內部狀態,依次遍歷每個狀態;可以根據需要,輕鬆地讓函數暫停執行或者繼續執行。
根據這個特點,我們可以利用Generator函數來實現異步操作的效果。
原理是:利用Generator函數暫停執行的作用,可以將異步操作的語句寫到yield後面,通過執行next方法進行回調。