本文是 重溫基礎 系列文章的第十三篇。
今日感受:每次自我年終總結,都會有各種情緒和收穫。
系列目錄:
- 【複習資料】ES6/ES7/ES8/ES9資料整理(個人整理)
- 【重溫基礎】1.語法和資料型別
- 【重溫基礎】2.流程控制和錯誤處理
- 【重溫基礎】3.迴圈和迭代
- 【重溫基礎】4.函式
- 【重溫基礎】5.表示式和運算子
- 【重溫基礎】6.數字
- 【重溫基礎】7.時間物件
- 【重溫基礎】8.字串
- 【重溫基礎】9.正規表示式
- 【重溫基礎】10.陣列
- 【重溫基礎】11.Map和Set物件
- 【重溫基礎】12.使用物件
本章節複習的是JS中的迭代器和生成器,常常用來處理集合。
前置知識:
JavaScrip已經提供多個迭代集合的方法,從簡單的for
迴圈到map()
和filter()
。
迭代器和生成器將迭代的概念直接帶入核心語言,並提供一種機制來自定義for...of
迴圈的行為。
本文會將知識點分為兩大部分,簡單介紹和詳細介紹:
簡單介紹,適合基礎入門會使用的目標;
詳細介紹,會更加深入的做介紹,適合理解原理;
1. 概述
當我們使用迴圈語句迭代資料時,需初始化一個變數來記錄每一次迭代在資料集合中的位置:
let a = ["aaa","bbb","ccc"];
for (let i = 0; i< a.length; i++){
console.log(a[i]);
}
這邊的i
就是我們用來記錄迭代位置的變數,但是在ES6開始,JavaScrip引入了迭代器這個特性,並且新的陣列方法和新的集合型別(如Set集合
與Map集合
)都依賴迭代器的實現,這個新特性對於高效的資料處理而言是不可或缺的,在語言的其他特性中也都有迭代器的身影:新的for-of迴圈
、展開運算子(...
),甚至連非同步程式設計都可以使用迭代器。
本文主要會介紹ES6中新增的迭代器(Iterator)和生成器(Generator)。
2. 迭代器(簡單介紹)
迭代器是一種特殊物件,它具有一些專門為迭代過程設計的專有介面,所有的迭代器物件都有一個next()
方法,每次呼叫都會返回一個結果物件。
這個結果物件,有兩個屬性:
-
value
: 表示下一個將要返回的值。 -
done
: 一個布林值,若沒有更多可返回的資料時,值為true
,否則false
。
如果最後一個值返回後,再呼叫next()
,則返回的物件的done
值為true
,而value
值如果沒有值的話,返回的為undefined
。
ES5實現一個迭代器:
function myIterator(list){
var i = 0;
return {
next: function(){
var done = i >= list.length;
var value = !done ? list[i++] : undefined;
return {
done : done,
value : value
}
}
}
}
var iterator = myIterator([1,2,3]);
iterator.next(); // "{done: false, value: 1}"
iterator.next(); // "{done: false, value: 2}"
iterator.next(); // "{done: false, value: 3}"
iterator.next(); // "{done: true, value: undefined}"
// 以後的呼叫都一樣
iterator.next(); // "{done: true, value: undefined}"
從上面程式碼可以看出,ES5的實現還是比較麻煩,而ES6新增的生成器,可以使得建立迭代器物件的過程更加簡單。
3. 生成器(簡單介紹)
生成器是一種返回迭代器的函式,通過function
關鍵字後的星號(*
)來表示,函式中會用到新的關鍵字yield
。星號可以緊挨著function
關鍵字,也可以在中間新增一個空格。
function *myIterator(){
yield 1;
yield 2;
yield 3;
}
let iterator = myIterator();
iterator.next(); // "{done: false, value: 1}"
iterator.next(); // "{done: false, value: 2}"
iterator.next(); // "{done: false, value: 3}"
iterator.next(); // "{done: true, value: undefined}"
// 以後的呼叫都一樣
iterator.next(); // "{done: true, value: undefined}"
生成器函式最有趣的部分是,每當執行完一條yield
語句後函式就會自動停止執行,比如上面程式碼,當yield 1;
執行完後,便不會執行任何語句,而是等到再呼叫迭代器的next()
方法才會執行下一個語句,即yield 2;
.
使用yield
關鍵字可以返回任何值和表示式,因為可以通過生成器函式批量給迭代器新增元素:
function *myIterator(list){
for(let i = 0; i< list.length ; i ++){
yield list[i];
}
}
var iterator = myIterator([1,2,3]);
iterator.next(); // "{done: false, value: 1}"
iterator.next(); // "{done: false, value: 2}"
iterator.next(); // "{done: false, value: 3}"
iterator.next(); // "{done: true, value: undefined}"
// 以後的呼叫都一樣
iterator.next(); // "{done: true, value: undefined}"
生成器的適用返回很廣,可以將它用於所有支援函式使用的地方。
4. 迭代器(詳細介紹)
4.1 Iterator迭代器概念
Iterator是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成迭代操作(即依次處理該資料結構的所有成員)。
Iterator三個作用:
- 為各種資料結構,提供一個統一的、簡便的訪問介面;
- 使得資料結構的成員能夠按某種次序排列;
-
Iterator 介面主要供ES6新增的
for...of
消費;
4.2 Iterator迭代過程
- 建立一個指標物件,指向當前資料結構的起始位置。也就是說,迭代器物件本質上,就是一個指標物件。
- 第一次呼叫指標物件的
next
方法,可以將指標指向資料結構的第一個成員。 - 第二次呼叫指標物件的
next
方法,指標就指向資料結構的第二個成員。 - 不斷呼叫指標物件的
next
方法,直到它指向資料結構的結束位置。
每一次呼叫next
方法,都會返回資料結構的當前成員的資訊。具體來說,就是返回一個包含value
和done
兩個屬性的物件。
-
value
屬性是當前成員的值; -
done
屬性是一個布林值,表示迭代是否結束;
模擬next
方法返回值:
let f = function (arr){
var nextIndex = 0;
return {
next:function(){
return nextIndex < arr.length ?
{value: arr[nextIndex++], done: false}:
{value: undefined, done: true}
}
}
}
let a = f([`a`, `b`]);
a.next(); // { value: "a", done: false }
a.next(); // { value: "b", done: false }
a.next(); // { value: undefined, done: true }
4.3 預設Iterator介面
若資料可迭代,即一種資料部署了Iterator介面。
ES6中預設的Iterator介面部署在資料結構的Symbol.iterator
屬性,即如果一個資料結構具有Symbol.iterator
屬性,就可以認為是可迭代。 Symbol.iterator
屬性本身是函式,是當前資料結構預設的迭代器生成函式。執行這個函式,就會返回一個迭代器。至於屬性名Symbol.iterator
,它是一個表示式,返回Symbol
物件的iterator
屬性,這是一個預定義好的、型別為 Symbol 的特殊值,所以要放在方括號內(參見《Symbol》一章)。
原生具有Iterator介面的資料結構有:
- Array
- Map
- Set
- String
- TypedArray
- 函式的 arguments 物件
- NodeList 物件
4.4 Iterator使用場景
- (1)解構賦值
對陣列和 Set
結構進行解構賦值時,會預設呼叫Symbol.iterator
方法。
let a = new Set().add(`a`).add(`b`).add(`c`);
let [x, y] = a; // x = `a` y = `b`
let [a1, ...a2] = a; // a1 = `a` a2 = [`b`,`c`]
- (2)擴充套件運算子
擴充套件運算子(...
)也會呼叫預設的 Iterator 介面。
let a = `hello`;
[...a]; // [`h`,`e`,`l`,`l`,`o`]
let a = [`b`, `c`];
[`a`, ...a, `d`]; // [`a`, `b`, `c`, `d`]
- (2)yield*
yield*
後面跟的是一個可迭代的結構,它會呼叫該結構的迭代器介面。
let a = function*(){
yield 1;
yield* [2,3,4];
yield 5;
}
let b = a();
b.next() // { value: 1, done: false }
b.next() // { value: 2, done: false }
b.next() // { value: 3, done: false }
b.next() // { value: 4, done: false }
b.next() // { value: 5, done: false }
b.next() // { value: undefined, done: true }
- (4)其他場合
由於陣列的迭代會呼叫迭代器介面,所以任何接受陣列作為引數的場合,其實都呼叫了迭代器介面。下面是一些例子。
- for…of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([[`a`,1],[`b`,2]])
) - Promise.all()
- Promise.race()
4.5 for…of迴圈
只要資料結構部署了Symbol.iterator
屬性,即具有 iterator 介面,可以用for...of
迴圈迭代它的成員。也就是說,for...of
迴圈內部呼叫的是資料結構的Symbol.iterato
方法。
使用場景: for...of
可以使用在陣列,Set
和Map
結構,類陣列物件,Genetator物件和字串。
- 陣列
for...of
迴圈可以代替陣列例項的forEach
方法。
let a = [`a`, `b`, `c`];
for (let k of a){console.log(k)}; // a b c
a.forEach((ele, index)=>{
console.log(ele); // a b c
console.log(index); // 0 1 2
})
與for...in
對比,for...in
只能獲取物件鍵名,不能直接獲取鍵值,而for...of
允許直接獲取鍵值。
let a = [`a`, `b`, `c`];
for (let k of a){console.log(k)}; // a b c
for (let k in a){console.log(k)}; // 0 1 2
- Set和Map
可以使用陣列作為變數,如for (let [k,v] of b){...}
。
let a = new Set([`a`, `b`, `c`]);
for (let k of a){console.log(k)}; // a b c
let b = new Map();
b.set(`name`,`leo`);
b.set(`age`, 18);
b.set(`aaa`,`bbb`);
for (let [k,v] of b){console.log(k + ":" + v)};
// name:leo
// age:18
// aaa:bbb
- 類陣列物件
// 字串
let a = `hello`;
for (let k of a ){console.log(k)}; // h e l l o
// DOM NodeList物件
let b = document.querySelectorAll(`p`);
for (let k of b ){
k.classList.add(`test`);
}
// arguments物件
function f(){
for (let k of arguments){
console.log(k);
}
}
f(`a`,`b`); // a b
- 物件
普通物件不能直接使用for...of
會報錯,要部署Iterator才能使用。
let a = {a:`aa`,b:`bb`,c:`cc`};
for (let k in a){console.log(k)}; // a b c
for (let k of a){console>log(k)}; // TypeError
4.6 跳出for…of
使用break
來實現。
for (let k of a){
if(k>100)
break;
console.log(k);
}
5. 生成器(詳細介紹)
5.1 基本概念
Generator
生成器函式是一種非同步程式設計解決方案。
原理:
執行Genenrator
函式會返回一個遍歷器物件,依次遍歷Generator
函式內部的每一個狀態。 Generator
函式是一個普通函式,有以下兩個特徵:
-
function
關鍵字與函式名之間有個星號; - 函式體內使用
yield
表示式,定義不同狀態;
通過呼叫next
方法,將指標移向下一個狀態,直到遇到下一個yield
表示式(或return
語句)為止。簡單理解,Generator
函式分段執行,yield
表示式是暫停執行的標記,而next
恢復執行。
function * f (){
yield `hi`;
yield `leo`;
return `ending`;
}
let a = f();
a.next(); // {value: `hi`, done : false}
a.next(); // {value: `leo`, done : false}
a.next(); // {value: `ending`, done : true}
a.next(); // {value: undefined, done : false}
5.2 yield表示式
yield
表示式是暫停標誌,遍歷器物件的next
方法的執行邏輯如下:
- 遇到
yield
就暫停執行,將這個yield
後的表示式的值,作為返回物件的value
屬性值。 - 下次呼叫
next
往下執行,直到遇到下一個yield
。 - 直到函式結束或者
return
為止,並返回return
語句後面表示式的值,作為返回物件的value
屬性值。 - 如果該函式沒有
return
語句,則返回物件的value
為undefined
。
注意:
-
yield
只能用在Generator
函式裡使用,其他地方使用會報錯。
// 錯誤1
(function(){
yiled 1; // SyntaxError: Unexpected number
})()
// 錯誤2 forEach引數是個普通函式
let a = [1, [[2, 3], 4], [5, 6]];
let f = function * (i){
i.forEach(function(m){
if(typeof m !== `number`){
yield * f (m);
}else{
yield m;
}
})
}
for (let k of f(a)){
console.log(k)
}
-
yield
表示式如果用於另一個表示式之中,必須放在圓括號內。
function * a (){
console.log(`a` + yield); // SyntaxErro
console.log(`a` + yield 123); // SyntaxErro
console.log(`a` + (yield)); // ok
console.log(`a` + (yield 123)); // ok
}
-
yield
表示式用做函式引數或放在表示式右邊,可以不加括號。
function * a (){
f(yield `a`, yield `b`); // ok
lei i = yield; // ok
}
5.3 next方法
yield
本身沒有返回值,或者是總返回undefined
,next
方法可帶一個引數,作為上一個yield
表示式的返回值。
function * f (){
for (let k = 0; true; k++){
let a = yield k;
if(a){k = -1};
}
}
let g =f();
g.next(); // {value: 0, done: false}
g.next(); // {value: 1, done: false}
g.next(true); // {value: 0, done: false}
這一特點,可以讓Generator
函式開始執行之後,可以從外部向內部注入不同值,從而調整函式行為。
function * f(x){
let y = 2 * (yield (x+1));
let z = yield (y/3);
return (x + y + z);
}
let a = f(5);
a.next(); // {value : 6 ,done : false}
a.next(); // {value : NaN ,done : false}
a.next(); // {value : NaN ,done : true}
// NaN因為yeild返回的是物件 和數字計算會NaN
let b = f(5);
b.next(); // {value : 6 ,done : false}
b.next(12); // {value : 8 ,done : false}
b.next(13); // {value : 42 ,done : false}
// x 5 y 24 z 13
5.4 for…of迴圈
for...of
迴圈會自動遍歷,不用呼叫next
方法,需要注意的是,for...of
遇到next
返回值的done
屬性為true
就會終止,return
返回的不包括在for...of
迴圈中。
function * f(){
yield 1;
yield 2;
yield 3;
yield 4;
return 5;
}
for (let k of f()){
console.log(k);
}
// 1 2 3 4 沒有 5
5.5 Generator.prototype.throw()
throw
方法用來向函式外丟擲錯誤,並且在Generator函式體內捕獲。
let f = function * (){
try { yield }
catch (e) { console.log(`內部捕獲`, e) }
}
let a = f();
a.next();
try{
a.throw(`a`);
a.throw(`b`);
}catch(e){
console.log(`外部捕獲`,e);
}
// 內部捕獲 a
// 外部捕獲 b
5.6 Generator.prototype.return()
return
方法用來返回給定的值,並結束遍歷Generator函式,如果return
方法沒有引數,則返回值的value
屬性為undefined
。
function * f(){
yield 1;
yield 2;
yield 3;
}
let g = f();
g.next(); // {value : 1, done : false}
g.return(`leo`); // {value : `leo`, done " true}
g.next(); // {value : undefined, done : true}
5.7 next()/throw()/return()共同點
相同點就是都是用來恢復Generator函式的執行,並且使用不同語句替換yield
表示式。
-
next()
將yield
表示式替換成一個值。
let f = function * (x,y){
let r = yield x + y;
return r;
}
let g = f(1, 2);
g.next(); // {value : 3, done : false}
g.next(1); // {value : 1, done : true}
// 相當於把 let r = yield x + y;
// 替換成 let r = 1;
-
throw()
將yield
表示式替換成一個throw
語句。
g.throw(new Error(`報錯`)); // Uncaught Error:報錯
// 相當於將 let r = yield x + y
// 替換成 let r = throw(new Error(`報錯`));
-
next()
將yield
表示式替換成一個return
語句。
g.return(2); // {value: 2, done: true}
// 相當於將 let r = yield x + y
// 替換成 let r = return 2;
5.8 yield* 表示式
用於在一個Generator中執行另一個Generator函式,如果沒有使用yield*
會沒有效果。
function * a(){
yield 1;
yield 2;
}
function * b(){
yield 3;
yield * a();
yield 4;
}
// 等同於
function * b(){
yield 3;
yield 1;
yield 2;
yield 4;
}
for(let k of b()){console.log(k)}
// 3
// 1
// 2
// 4
5.9 應用場景
- 控制流管理
解決回撥地獄:
// 使用前
f1(function(v1){
f2(function(v2){
f3(function(v3){
// ... more and more
})
})
})
// 使用Promise
Promise.resolve(f1)
.then(f2)
.then(f3)
.then(function(v4){
// ...
},function (err){
// ...
}).done();
// 使用Generator
function * f (v1){
try{
let v2 = yield f1(v1);
let v3 = yield f1(v2);
let v4 = yield f1(v3);
// ...
}catch(err){
// console.log(err)
}
}
function g (task){
let obj = task.next(task.value);
// 如果Generator函式未結束,就繼續呼叫
if(!obj.done){
task.value = obj.value;
g(task);
}
}
g( f(initValue) );
- 非同步程式設計的使用
在真實的非同步任務封裝的情況:
let fetch = require(`node-fetch`);
function * f(){
let url = `http://www.baidu.com`;
let res = yield fetch(url);
console.log(res.bio);
}
// 執行該函式
let g = f();
let result = g.next();
// 由於fetch返回的是Promise物件,所以用then
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
})
參考資料
1.MDN 迭代器和生成器
2.ES6中的迭代器(Iterator)和生成器(Generator)
本部分內容到這結束
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推薦 | https://github.com/pingan8787… |
JS小冊 | js.pingan8787.com |
歡迎關注微信公眾號【前端自習課】每天早晨,與您一起學習一篇優秀的前端技術博文 .