- 1. 前言
- 2. 一、Promise
- 2.1. 1. 執行順序
- 2.2. 2.異步加載圖片:
- 2.3. 3.用Promise對象實現Ajax:
- 2.4. 4.then方法返回的是一個新的Promise實例(不是原來那個Promise實例)
- 2.5. 5.如果 Promise 狀態已經變成resolved,再拋出錯誤是無效的:
- 2.6. 6.跟傳統的try/catch代碼塊不同的是,如果沒有使用catch方法指定錯誤處理的回調函數,Promise 對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應,通俗的說法就是“Promise 會吃掉錯誤”:
- 2.7. 7.
- 2.8. 8.Promise.prototype.finally()
- 2.9. 9.Promise.all(數組或具有 Iterator 接口,且返回的每個成員都是 Promise 實例)
- 2.10. 10.Promise.race
- 2.11. 11.Promise.resolve()
- 2.12. 12.Promise.reject()
- 2.13. 13.如果對於一個函數,不管是同步或異步,都想使用then方法指定下一流程,可使用以下方式,讓它是同步時就按同步執行,是異步時就按異步執行:
- 3. 二、Iterator和for of循環
- 3.1. 1. Iterator(遍歷器)的概念
- 3.2. 2.ES6 規定,默認的 Iterator 接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具有Symbol.iterator屬性,就可以認為是“可遍歷的”(iterable)。Symbol.iterator屬性本身是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。至於屬性名Symbol.iterator,它是一個表達式,返回Symbol對象的iterator屬性,這是一個預定義好的、類型為 Symbol 的特殊值,所以要放在方括號內。
- 3.3. 3.原生具備 Iterator 接口的數據結構如下:不含對象
- 3.4. 4.數組的Symbol.iterator屬性:
- 3.5. 5.對象(Object)之所以沒有默認部署 Iterator 接口,是因為對象的哪個屬性先遍歷,哪個屬性後遍歷是不確定的,需要開發者手動指定。本質上,遍歷器是一種線性處理,對於任何非線性的數據結構,部署遍歷器接口,就等於部署一種線性轉換。不過,嚴格地說,對象部署遍歷器接口並不是很必要,因為這時對象實際上被當作 Map 結構使用,ES5 沒有 Map 結構,而 ES6 原生提供了。
- 3.6. 6.有一些場合會默認調用 Iterator 接口(即Symbol.iterator方法):
- 3.7. 7. Iterator接口與Generator函數
- 3.8. 8.for…of循環調用遍歷器接口,數組的遍歷器接口只返回具有數字索引的屬性。這一點跟for…in循環也不一樣。
- 3.9. 9. Map遍歷得到的是數組,Set遍歷得到的是單個值:
- 3.10. 10. 可用Array.from將不具有iterator接口的類數組對象轉為數組,這樣也就具有了iterator接口:
- 3.11. 11.循環對比:
- 4. 三、Generator
- 4.1. 1. 執行Generator(生成器)返回一個遍歷器對象,這個遍歷器對象可以依次遍歷Generator函數內部的每一個狀態,yield表達式,定義不同的內部狀態(yield在英語裡的意思就是“產出”):
- 4.2. 2. 遍歷器對象的next方法的運行邏輯如下:
- 4.3. 3. Generator 函數可以不用yield表達式,這時就變成了一個單純的暫緩執行函數。
- 4.4. 4. yield表達式如果用在另一個表達式之中,必須放在圓括號裡面:
- 4.5. 5.與 Iterator 接口的關係:
- 4.6. 6.yield表達式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值。
- 4.7. 7. for of 循環:
- 4.8. 8. Generator.prototype.throw()
- 4.9. 9. throw方法被捕獲以後,會附帶執行下一條yield表達式。也就是說,會附帶執行一次next方法:
- 4.10. 10. Generator.prototype.return()
- 4.11. 11. 在一個 Generator 函數裡面執行另一個 Generator 函數:
- 5. 四、async
- 5.1. 1. async 函數其實就是 Generator 函數的語法糖,可以再等待第一階段得到結果後自動執行第二階段,而不是像Generator那樣手動執行。*換成了async,yield換成了await。
- 5.2. 2. async函數對 Generator 函數的改進,體現在以下加點:
- 5.3. 3. async函數返回一個 Promise 對象。
- 5.4. 4. Promise 對象的狀態變化
- 5.5. 5. 一般await後面是接promise對象,返回該對象的結果,如果不是promise對象,則直接返回對應的值:
- 5.6. 6. await後面的promise狀態如果為reject,則會被catch到:
- 5.7. 7. 使用async注意點:
- 5.8. 8. async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數裡:
- 5.9. 9. 繼發/併發順序輸出:
- 5.10. 10. 異步遍歷器:asyncIterator,部署在Symbol.asyncIterator屬性上面,最大的語法特點就是調用遍歷器的next方法,返回的是一個 Promise 對象。
- 5.11. 11. for await…of
- 5.12. 12. 異步 Generator 函數
- 5.13. 13. yield* 語句
- 6. 最後
- 7. 相關文章
前言
前段時間整理了ES6的讀書筆記:《ES6讀書筆記(一)》,《ES6讀書筆記(二)》,現在為第三篇,本篇內容包括:
- 一、Promise
- 二、Iterator和for of循環
- 三、Generator
- 四、async
本文筆記也主要是根據阮一峰老師的《ECMAScript 6 入門》和平時的理解進行整理的,希望對你有所幫助,喜歡的就點個贊吧!
一、Promise
1. 執行順序
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
2.異步加載圖片:
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = function() {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}
3.用Promise對象實現Ajax:
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出錯了', error);
});
4.then方法返回的是一個新的Promise實例(不是原來那個Promise實例)
5.如果 Promise 狀態已經變成resolved,再拋出錯誤是無效的:
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代碼中,Promise 在resolve語句後面,再拋出錯誤,不會被捕獲,等於沒有拋出。因為 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了
6.跟傳統的try/catch代碼塊不同的是,如果沒有使用catch方法指定錯誤處理的回調函數,Promise 對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應,通俗的說法就是“Promise 會吃掉錯誤”:
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
resolve(x + 2); // 會報錯,因為x沒有聲明
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000); // 雖然以上有錯誤,但沒有阻塞後面的代碼
// Uncaught (in promise) ReferenceError: x is not defined
// 123
7.
const promise = new Promise(function (resolve, reject) {
resolve('ok');
setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test
上面代碼中,Promise 指定在下一輪“事件循環”再拋出錯誤。到了那個時候,Promise 的運行已經結束了,所以這個錯誤是在 Promise 函數體外拋出的,會冒泡到最外層,成了未捕獲的錯誤,相當於是js引擎去執行了這個回調,而不是在promise內部執行。
一般總是建議,Promise 對象後面要跟catch方法,這樣可以處理 Promise 內部發生的錯誤。catch方法返回的還是一個 Promise 對象,因此後面還可以接著調用then方法:
- ①如果有錯誤,但沒有去catch,則會阻塞promise內部的代碼,但不會阻塞外部的代碼;
- ②如果有catch,但是沒有錯誤,則會跳過catch,繼續執行後面的代碼;
- ③如果有catch,然後被catch捕獲了錯誤,那依舊可以繼續執行後面的代碼;
- ④如果有catch,catch捕獲到了前面的錯誤,但catch內部又有錯誤的話,則會阻塞後面的代碼,除非後面再鏈式調用catch捕獲該錯誤。
以上總結就是隻要promise內部有錯誤沒有被捕獲,就會阻塞內部代碼,但不會阻塞外部代碼。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,因為x沒有聲明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
//--------------------------------------------
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,因為x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
y + 2; // y 沒有聲明會報錯,且這個錯誤未被捕獲,會阻塞後面的代碼
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
8.Promise.prototype.finally()
- ①finally方法用於指定不管 Promise 對象最後狀態如何,都會執行的操作:
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代碼中,不管promise最後的狀態,在執行完then或catch指定的回調函數以後,都會執行finally方法指定的回調函數。
②finally方法的回調函數不接受任何參數,這意味著沒有辦法知道,前面的 Promise 狀態到底是fulfilled還是rejected。這表明,finally方法裡面的操作,應該是與狀態無關的,不依賴於 Promise 的執行結果。
③finally本質上是then方法的特例:
promise
.finally(() => {
// 語句
});
// 等同於
promise
.then(
result => {
// 語句
return result;
},
error => {
// 語句
throw error;
}
);
- ④finally方法總是會返回原來的值:
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
9.Promise.all(數組或具有 Iterator 接口,且返回的每個成員都是 Promise 實例)
①如果參數全為fulfilled,則返回對應的數組結果(是等全部得到結果了再一起返回),但如果有一個是rejected,則返回第一個rejected的返回值,狀態就為rejected。
②catch後會變為resolved:
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
// reject(“world”);
})
.then(result => result)
.catch(e => e); // catch後會變為resolved
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e)); // 傳入的p2有自己的catch,所以不會觸發這裡的catch,所以沒有捕獲到錯誤,所以就相當於都是執行正確的,所以會有結果
// ["hello", Error: 報錯了]
10.Promise.race
參數中誰率先改變了狀態,就返回誰的狀態,這意味著只返回一個結果
11.Promise.resolve()
- ①Promise.resolve方法允許調用時不帶參數,直接返回一個resolved狀態的 Promise 對象。
所以,如果希望得到一個 Promise 對象,比較方便的方法就是直接調用Promise.resolve方法:
const p = Promise.resolve();
p.then(function () {
// ...
});
- ②立即resolve的 Promise 對象,是在本輪“事件循環”(event loop)的結束時,而不是在下一輪“事件循環”的開始時。
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
12.Promise.reject()
Promise.reject()方法的參數,會原封不動地作為reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致:
const thenable = {
then(resolve, reject) {
reject('出錯了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面代碼中,Promise.reject方法的參數是一個thenable對象,執行以後,後面catch方法的參數不是reject拋出的“出錯了”這個字符串,而是thenable對象。
13.如果對於一個函數,不管是同步或異步,都想使用then方法指定下一流程,可使用以下方式,讓它是同步時就按同步執行,是異步時就按異步執行:
不要直接使用promise.resolve(),因為如果是同步函數,會在本輪事件循環末尾才會執行:
const f = () => console.log('now');
Promise.resolve().then(f); // then才是微任務,resolve時還是同步的
console.log('next');
// next
// now
- ①使用async:
const f = () => console.log('now');
(async () => f())()
.then(...)
.catch(...);
console.log('next');
// now
// next
- ②使用new Promise():
const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
- ③一個提案,提供Promise.try方法替代上面的寫法:瀏覽器目前會報錯
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
二、Iterator和for of循環
1. Iterator(遍歷器)的概念
JavaScript 原有的表示“集合”的數據結構,主要是數組(Array)和對象(Object),ES6 又添加了Map和Set。這樣就有了四種數據集合,用戶還可以組合使用它們,定義自己的數據結構,比如數組的成員是Map,Map的成員是對象。這樣就需要一種統一的接口機制,來處理所有不同的數據結構。
Iterator 接口是一種數據遍歷的協議,只要調用遍歷器對象的next方法,就會得到一個對象,表示當前遍歷指針所在的那個位置的信息。
遍歷器(Iterator)就是這樣一種機制。它是一種接口,為各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。
Iterator 的作用有三個:一是為各種數據結構,提供一個統一的、簡便的訪問接口;二是使得數據結構的成員能夠按某種次序排列;三是 ES6 創造了一種新的遍歷命令for…of循環,Iterator 接口主要供for…of消費。
Iterator 的遍歷過程是這樣的:
- (1)創建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
- (2)第一次調用指針對象的next方法,可以將指針指向數據結構的第一個成員。
- (3)第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
- (4)不斷調用指針對象的next方法,直到它指向數據結構的結束位置。
每一次調用next方法,都會返回數據結構的當前成員的信息。具體來說,就是返回一個包含value和done兩個屬性的對象。其中,value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。
一個模擬next方法返回值的例子:
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
2.ES6 規定,默認的 Iterator 接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具有Symbol.iterator屬性,就可以認為是“可遍歷的”(iterable)。Symbol.iterator屬性本身是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。至於屬性名Symbol.iterator,它是一個表達式,返回Symbol對象的iterator屬性,這是一個預定義好的、類型為 Symbol 的特殊值,所以要放在方括號內。
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
3.原生具備 Iterator 接口的數據結構如下:不含對象
- Array
- Map
- Set
- String
- TypedArray
- 函數的 arguments 對象
- NodeList 對象
4.數組的Symbol.iterator屬性:
let arr = ['a', 'b', 'c'];
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 }
5.對象(Object)之所以沒有默認部署 Iterator 接口,是因為對象的哪個屬性先遍歷,哪個屬性後遍歷是不確定的,需要開發者手動指定。本質上,遍歷器是一種線性處理,對於任何非線性的數據結構,部署遍歷器接口,就等於部署一種線性轉換。不過,嚴格地說,對象部署遍歷器接口並不是很必要,因為這時對象實際上被當作 Map 結構使用,ES5 沒有 Map 結構,而 ES6 原生提供了。
6.有一些場合會默認調用 Iterator 接口(即Symbol.iterator方法):
- ①解構賦值
- ②擴展運算符:這樣就可對有Iterator接口的數據結構使用擴展運算符轉為數組,而對於沒有Iterator接口的類數組,可採用Array.from轉為數組,這樣就具有了Iterator接口
- ③yield*
yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口:
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
④其他場合
由於數組的遍歷會調用遍歷器接口,所以任何接受數組作為參數的場合,其實都調用了遍歷器接口:for…of
Array.from()
Map(), Set(), WeakMap(), WeakSet()(比如new Map([[‘a’,1],[‘b’,2]]))
Promise.all()
Promise.race()
7. Iterator接口與Generator函數
Symbol.iterator方法的最簡單實現:
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
}
[...myIterable] // [1, 2, 3]
// 或者採用下面的簡潔寫法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// "hello"
// "world"
8.for…of循環調用遍歷器接口,數組的遍歷器接口只返回具有數字索引的屬性。這一點跟for…in循環也不一樣。
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
console.log(i); // "0", "1", "2", "foo" 這也說明了for in遍歷了自身及原型上的可枚舉屬性
}
for (let i of arr) {
console.log(i); // "3", "5", "7"
}
9. Map遍歷得到的是數組,Set遍歷得到的是單個值:
let map = new Map().set('a', 1).set('b', 2);
Map; // {"a" => 1, "b" => 2}
for (let pair of map) {
console.log(pair);
}
// ['a', 1]
// ['b', 2]
for (let [key, value] of map) {
console.log(key + ' : ' + value);
}
// a : 1
// b : 2
10. 可用Array.from將不具有iterator接口的類數組對象轉為數組,這樣也就具有了iterator接口:
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 報錯
for (let x of arrayLike) {
console.log(x);
}
// 正確
for (let x of Array.from(arrayLike)) {
console.log(x);
}
11.循環對比:
- for in 會遍歷原型可枚舉屬性,為遍歷對象而生,儘管對象沒有iterator接口
- forEach不能中途跳出循環
- for of 可中途跳出循環,不會遍歷原型可枚舉屬性,針對數組
三、Generator
1. 執行Generator(生成器)返回一個遍歷器對象,這個遍歷器對象可以依次遍歷Generator函數內部的每一個狀態,yield表達式,定義不同的內部狀態(yield在英語裡的意思就是“產出”):
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next() // 遇到第一個yield,暫停,然後返回yield後面的表達式的值
// { value: 'hello', done: false }
hw.next() // 從上次暫停的地方往下執行,遇到第二個yield後暫停,返回值
// { value: 'world', done: false }
hw.next() // 從上次暫停的地方往下執行,發現沒有yield了,所以一直往下執行,直到遇到// return,如果沒有return則返回undefined
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
定義方式:
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· } // 推薦這種寫法
function*foo(x, y) { ··· }
2. 遍歷器對象的next方法的運行邏輯如下:
(1)遇到yield表達式,就暫停執行後面的操作,並將緊跟在yield後面的那個表達式的值,作為返回的對象的value屬性值。
(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
(3)如果沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句為止,並將return語句後面的表達式的值,作為返回的對象的value屬性值。
(4)如果該函數沒有return語句,則返回的對象的value屬性值為undefined。
需要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時才會執行,因此等於為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。
3. Generator 函數可以不用yield表達式,這時就變成了一個單純的暫緩執行函數。
function* f() {
console.log('執行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
4. yield表達式如果用在另一個表達式之中,必須放在圓括號裡面:
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
yield表達式用作函數參數或放在賦值表達式的右邊,可以不加括號:
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
5.與 Iterator 接口的關係:
任意一個對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。
由於 Generator 函數就是遍歷器生成函數,因此可以把 Generator 賦值給對象的Symbol.iterator屬性,從而使得該對象具有 Iterator 接口:
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
上面代碼中,Generator 函數賦值給Symbol.iterator屬性,從而使得myIterable對象具有了 Iterator 接口,可以被…運算符遍歷了。
Generator 函數執行後,返回一個遍歷器對象。該對象本身也具有Symbol.iterator屬性,執行後返回自身:
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
6.yield表達式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i; // yield i表達式是沒有返回值的,或者說返回undefined
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false } 相當於給reset賦值為true,重置了i的值
next參數的值是傳給上一個yield表達式的返回值,所以這也意味著第一個next的參數是無效的,所以不需要傳,即第一個next是用於啟動遍歷器對象:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false} yield (x + 1)的返回值是undefined,所以乘2再除3得到的是NaN
a.next() // Object{value:NaN, done:true} 5 + NaN + undefined為NaN
var b = foo(5);
b.next() // { value:6, done:false } 5+1得到6
b.next(12) // { value:8, done:false } 12賦給yield (x + 1),然後乘2除3得到8,即(y / 3)的值
b.next(13) // { value:42, done:true } 同理
//-----------------------------------------
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
// {value: undefined, done: false}
genObj.next('a')
// 1. a
// {value: undefined, done: false}
genObj.next('b')
// 2. b
// {value: "result", done: true}
7. for of 循環:
for…of循環可以自動遍歷 Generator 函數時生成的Iterator對象,且此時不再需要調用next方法:
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5 沒有6,因為一旦next方法的返回對象的done屬性為true,for...of循環就會中止,且不包含該返回對象
8. Generator.prototype.throw()
Generator 函數返回的遍歷器對象,都有一個throw方法,可以在函數體外拋出錯誤,然後在 Generator 函數體內捕獲:
var g = function* () {
try {
yield;
} catch (e) {
console.log('內部捕獲', e);
}
};
var i = g();
i.next(); // 要捕獲錯誤,必須先執行一次next來啟動遍歷器對象
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
9. throw方法被捕獲以後,會附帶執行下一條yield表達式。也就是說,會附帶執行一次next方法:
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
10. Generator.prototype.return()
Generator 函數返回的遍歷器對象,還有一個return方法,可以返回給定的值,並且終結遍歷 Generator 函數:
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果return方法調用時,不提供參數,則返回值的value屬性為undefined:
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
如果 Generator 函數內部有try…finally代碼塊,且正在執行try代碼塊,那麼return方法會推遲到finally代碼塊執行完再執行。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
上面代碼中,調用return方法後,就開始執行finally代碼塊,然後等到finally代碼塊執行完,再執行return方法。
11. 在一個 Generator 函數裡面執行另一個 Generator 函數:
function* bar() {
yield 'x';
yield* foo(); // 加了*號就是返回遍歷器內部值,相當於調用了*後面變量的iterator接口,否則返回遍歷器對象
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
3.1 異步應用
1.異步簡單說就是不連續的執行任務,類似一個協程的過程:
function* asyncJob() {
// ...其他代碼
var f = yield readFile(fileA);
// ...其他代碼
}
其中的yield命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield命令是異步兩個階段的分界線,協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。
2.Generator 就是一個異步操作的容器。它的自動執行需要一種機制,當異步操作有了結果,能夠自動交回執行權。
3.傳值調用:先計算參數值再傳入函數體內使用。
傳名調用:直接將參數表達式傳入函數體內,使用到時再進行求值。
4.Generator的異步應用中何時調用第一步,何時調用第二步,此時就需要使用thunk函數,相當於“傳名調用”,編譯器的“傳名調用”實現,往往是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體,這個臨時函數就叫做 Thunk 函數:
function f(m) {
return m * 2;
}
f(x + 5);
// 等同於
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
5.JavaScript 語言的 Thunk 函數
JavaScript 語言是傳值調用,它的 Thunk 函數含義有所不同。在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數作為參數的單參數函數,類似柯里化:
// 正常版本的readFile(多參數版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(單參數版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
四、async
1. async 函數其實就是 Generator 函數的語法糖,可以再等待第一階段得到結果後自動執行第二階段,而不是像Generator那樣手動執行。*換成了async,yield換成了await。
2. async函數對 Generator 函數的改進,體現在以下加點:
- ①內置執行器
- ②更語義化
- ③適應性
- ④返回promise
3. async函數返回一個 Promise 對象。
async函數內部return語句返回的值,會成為then方法回調函數的參數:
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
如果沒有return,則then方法回調函數的參數則得到的是undefined
async function f() {
await Promise.resolve('hello world'); // 不會執行
}
f().then(a=>{console.log(a)}) // undefined
4. Promise 對象的狀態變化
async函數返回的 Promise 對象,必須等到內部所有await命令後面的 Promise 對象執行完,才會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操作執行完,才會執行then方法指定的回調函數:
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
上面代碼中,函數getTitle內部有三個操作:抓取網頁、取出文本、匹配頁面標題。只有這三個操作全部完成,才會執行then方法裡面的console.log
5. 一般await後面是接promise對象,返回該對象的結果,如果不是promise對象,則直接返回對應的值:
async function f() {
// 等同於
// return 123;
return await 123; // return要放await前面,否則會報錯
}
f().then(v => console.log(v))
// 123
6. await後面的promise狀態如果為reject,則會被catch到:
async function f() {
await Promise.reject('出錯了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了
任何一個await語句後面的 Promise 對象變為reject狀態,那麼整個async函數都會中斷執行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行
}
為了防止有錯誤或reject中斷代碼的執行,則需要使用catch來處理,或者使用try catch:
async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出錯了
// hello world
如果有多個await命令,可以統一放在try…catch結構中:
async function main() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}
7. 使用async注意點:
- ①catch錯誤,防止代碼中斷
- ②對於不存在繼發關係的異步操作,應該讓它們同步進行,而不是順序執行:
let foo = await getFoo();
let bar = await getBar();
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
- ③await命令只能用在async函數之中,如果用在普通函數,就會報錯,如用在forEach中會報錯,因為是併發執行,應該使用for循環:
unction dbFuc(db) { //這裡不需要 async
let docs = [{}, {}, {}];
// 可能得到錯誤結果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代碼可能不會正常工作,原因是這時三個db.post操作將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用for循環。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
- ④async 函數可以保留運行堆棧。
const a = () => {
b().then(() => c());
};
上面代碼中,函數a內部運行了一個異步任務b()。當b()運行的時候,函數a()不會中斷,而是繼續執行。等到b()運行結束,可能a()早就運行結束了,b()所在的上下文環境已經消失了。如果b()或c()報錯,錯誤堆棧將不包括a()。
改成async函數:
const a = async () => {
await b();
c();
};
上面代碼中,b()運行的時候,a()是暫停執行,上下文環境都保存著。一旦b()或c(),錯誤堆棧將包括a()。
8. async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數裡:
async function fn(args) {
// ...
}
// 等同於
function fn(args) {
return spawn(function* () {
// ...
});
}
9. 繼發/併發順序輸出:
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
上面代碼確實大大簡化,問題是所有遠程操作都是繼發。只有前一個 URL 返回結果,才會去讀取下一個 URL,這樣做效率很差,非常浪費時間。我們需要的是併發發出遠程請求:
async function logInOrder(urls) {
// 併發讀取遠程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序輸出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
10. 異步遍歷器:asyncIterator,部署在Symbol.asyncIterator屬性上面,最大的語法特點就是調用遍歷器的next方法,返回的是一個 Promise 對象。
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator
.next()
.then(iterResult1 => {
console.log(iterResult1); // { value: 'a', done: false }
return asyncIterator.next();
})
.then(iterResult2 => {
console.log(iterResult2); // { value: 'b', done: false }
return asyncIterator.next();
})
.then(iterResult3 => {
console.log(iterResult3); // { value: undefined, done: true }
});
可改寫為:
async function f() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next());
// { value: 'a', done: false }
console.log(await asyncIterator.next());
// { value: 'b', done: false }
console.log(await asyncIterator.next());
// { value: undefined, done: true }
}
異步遍歷器的next方法是可以連續調用的,不必等到上一步產生的 Promise 對象resolve以後再調用。這種情況下,next方法會累積起來,自動按照每一步的順序運行下去,所以也可以這樣:
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
asyncIterator.next(), asyncIterator.next()
]);
console.log(v1, v2); // a b
11. for await…of
for…of循環用於遍歷同步的 Iterator 接口。新引入的for await…of循環,則是用於遍歷異步的 Iterator 接口:
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
如果next方法返回的 Promise 對象被reject,for await…of就會報錯,要用try…catch捕捉。
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
注意,for await…of循環也可以用於同步遍歷器:
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b
12. 異步 Generator 函數
就像 Generator 函數返回一個同步遍歷器對象一樣,異步 Generator 函數的作用,是返回一個異步遍歷器對象:
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
13. yield* 語句
yield*語句也可以跟一個異步遍歷器:
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最終會等於 2
const result = yield* gen1();
}
與同步 Generator 函數一樣,for await…of循環會展開yield*:
(async function () {
for await (const x of gen2()) { // 也是相當於執行了gen的遍歷器
console.log(x);
}
})();
// a
// b
最後
因為比較多,所以目前只整理到這裡,後續有些比較重要難懂的模塊會分開更新,同時包括ES6的部分,希望對你有所幫助,如有不合理的地方歡迎指正,喜歡的就關注一波吧,後續會持續更新。
