ES6讀書筆記(三)

NO IMAGE
目錄

前言

前段時間整理了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的部分,希望對你有所幫助,如有不合理的地方歡迎指正,喜歡的就關注一波吧,後續會持續更新。

ES6讀書筆記(三)

相關文章

前端重點之數據處理

HTTP解析

Vue源碼該如何入手?

理理Vue細節