1. 程式人生 > >ES6語法筆記(Iterator、for...of、Generator、async)

ES6語法筆記(Iterator、for...of、Generator、async)

**以下內容均摘自ECMAScript 6 入門——阮一峰

一、Iterator

遍歷器(Iterator)是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作。

Iterator 的作用有三個:一是為各種資料結構,提供一個統一的、簡便的訪問介面;二是使得資料結構的成員能夠按某種次序排列;三是 ES6 創造了一種新的遍歷命令for...of迴圈,Iterator 介面主要供for...of消費。

Iterator 的遍歷過程是這樣的。

(1)建立一個指標物件,指向當前資料結構的起始位置。也就是說,遍歷器物件本質上,就是一個指標物件。

(2)第一次呼叫指標物件的next方法,可以將指標指向資料結構的第一個成員。

(3)第二次呼叫指標物件的next方法,指標就指向資料結構的第二個成員。

(4)不斷呼叫指標物件的next方法,直到它指向資料結構的結束位置。

每一次呼叫next方法,都會返回資料結構的當前成員的資訊。具體來說,就是返回一個包含valuedone兩個屬性的物件。其中,value屬性是當前成員的值,done屬性是一個布林值,表示遍歷是否結束。

ES6 規定,預設的 Iterator 介面部署在資料結構的Symbol.iterator屬性,或者說,一個數據結構只要具有Symbol.iterator屬性,就可以認為是“可遍歷的”(iterable)。

原生具備 Iterator 介面的資料結構如下。

Array Map Set String TypedArray 函式的 arguments 物件 NodeList 物件

//變數arr是一個數組,原生就具有遍歷器介面,部署在arr的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 }

一個物件如果要具備可被for...of迴圈呼叫的 Iterator 介面,就必須在Symbol.iterator的屬性上部署遍歷器生成方法(原型鏈上的物件具有該方法也可)。

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value); // 0, 1, 2
}

字串是一個類似陣列的物件,也原生具有 Iterator 介面。

var someString = "hi";
typeof someString[Symbol.iterator]
// "function"

var iterator = someString[Symbol.iterator]();

iterator.next()  // { value: "h", done: false }
iterator.next()  // { value: "i", done: false }
iterator.next()  // { value: undefined, done: true }

Symbol.iterator方法的最簡單實現,是使用Generator 函式。

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"

 

 

二、for...of

一個數據結構只要部署了Symbol.iterator屬性,就被視為具有 iterator 介面,就可以用for...of迴圈遍歷它的成員。

for...of迴圈可以使用的範圍包括陣列、Set 和 Map 結構、某些類似陣列的物件(比如arguments物件、DOM NodeList 物件)、Generator 物件,以及字串。

JavaScript 原有的for...in迴圈,只能獲得物件的鍵名,不能直接獲取鍵值。ES6 提供for...of迴圈,允許遍歷獲得鍵值。

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
  console.log(e);
}
// Gecko
// Trident
// Webkit

var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
  console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262

並不是所有類似陣列的物件都具有 Iterator 介面,一個簡便的解決方法,就是使用Array.from方法將其轉為陣列。

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);
}

對於普通的物件,for...of結構不能直接使用,會報錯,必須部署了 Iterator 介面後才能使用。但是,這樣情況下,for...in迴圈依然可以用來遍歷鍵名。

let es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (let e in es6) {
  console.log(e);
}
// edition
// committee
// standard

for (let e of es6) {
  console.log(e);
}
// TypeError: es6[Symbol.iterator] is not a function

可以使用Object.keys方法將物件的鍵名生成一個數組,然後遍歷這個陣列。也可以使用 Generator 函式將物件重新包裝一下。

for (var key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}

function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}

for (let [key, value] of entries(obj)) {
  console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3

 

 

三、Generator

Generator 函式是 ES6 提供的一種非同步程式設計解決方案。語法上,首先可以把它理解成,Generator 函式是一個狀態機,封裝了多個內部狀態。

執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式除了狀態機,還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。

Generator 函式是一個普通函式,但是有兩個特徵。一是,function關鍵字與函式名之間有一個星號;二是,函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

呼叫 Generator 函式,返回一個遍歷器物件,代表 Generator 函式的內部指標。以後,每次呼叫遍歷器物件的next方法,就會返回一個有著valuedone兩個屬性的物件。value屬性表示當前的內部狀態的值,是yield表示式後面那個表示式的值;done屬性是一個布林值,表示是否遍歷結束。

由於 Generator 函式就是遍歷器生成函式,因此可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]

 

Generator 函式從暫停狀態到恢復執行,它的上下文狀態(context)是不變的。通過next方法的引數,就有辦法在 Generator 函式開始執行之後,繼續向函式體內部注入值。也就是說,可以在 Generator 函式執行的不同階段,從外部向內部注入不同的值,從而調整函式行為。

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}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

上面程式碼第一次呼叫bnext方法時,返回x+1的值6;第二次呼叫next方法,將上一次yield表示式的值設為12,因此y等於24,返回y / 3的值8;第三次呼叫next方法,將上一次yield表示式的值設為13,因此z等於13,這時x等於5y等於24,所以return語句的值等於42

從語義上講,第一個next方法用來啟動遍歷器物件,所以不用帶有引數。

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
//一旦next方法的返回物件的done屬性為true,for...of迴圈就會中止,且不包含該返回物件

Generator.prototype.throw()Generator 函式返回的遍歷器物件,都有一個throw方法,可以在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('內部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b

歷器物件i連續丟擲兩個錯誤。第一個錯誤被 Generator 函式體內的catch語句捕獲。i第二次丟擲錯誤,由於 Generator 函式內部的catch語句已經執行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被丟擲了 Generator 函式體,被函式體外的catch語句捕獲。

throw方法丟擲的錯誤要被內部捕獲,前提是必須至少執行過一次next方法。

throw方法被捕獲以後,會附帶執行下一條yield表示式。也就是說,會附帶執行一次next方法。

Generator.prototype.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 }
//如果 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 }

next()throw()return()這三個方法本質上是同一件事,可以放在一起理解。它們的作用都是讓 Generator 函式恢復執行,並且使用不同的語句替換yield表示式。

如果yield表示式後面跟的是一個遍歷器物件,需要在yield表示式後面加上星號,表明它返回的是一個遍歷器物件。這被稱為yield*表示式。

let delegatedIterator = (function* () {
  yield 'Hello!';
  yield 'Bye!';
}());

let delegatingIterator = (function* () {
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
//yield表示式返回整個字串,yield*語句返回單個字元。因為字串具有 Iterator 介面,所以被yield*遍歷。
let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"

物件的屬性是 Generator 函式,可以簡寫成下面的形式。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};
//等同於
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

Generator 函式不是建構函式。返回的總是遍歷器物件,而不是this物件。

function* g() {
  this.a = 11;
}

let obj = g();
obj.next();
obj.a // undefined

function* F() {
  yield this.x = 2;
  yield this.y = 3;
}

new F()
// TypeError: F is not a constructor

//下面將其改造成建構函式
function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權(即暫停執行)。

整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用yield語句註明。

Generator 函式可以暫停執行和恢復執行,這是它能封裝非同步任務的根本原因。除此之外,它還有兩個特性,使它可以作為非同步程式設計的完整解決方案:函式體內外的資料交換和錯誤處理機制。

next返回值的 value 屬性,是 Generator 函式向外輸出資料;next方法還可以接受引數,向 Generator 函式體內輸入資料。

Generator 函式內部還可以部署錯誤處理程式碼,捕獲函式體外丟擲的錯誤。

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});
//首先執行 Generator 函式,獲取遍歷器物件,然後使用next方法(第二行),執行非同步任務的第一階段。由於Fetch模組返回的是一個 Promise 物件,因此要用then方法呼叫下一個next方法。

Generator 非同步呼叫的自動執行器

基於 Thunk 函式的 Generator 自動執行器

基於co模組的Generator 自動執行器(實際上是基於Promise物件的自動執行)

 

 

四、async

async 函式是什麼?一句話,它就是 Generator 函式的語法糖。

async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await,僅此而已。

async函式的執行,與普通函式一模一樣,只要一行。

async函式返回一個 Promise 物件,可以使用then方法添加回調函式。

// 函式宣告
async function foo() {}

// 函式表示式
const foo = async function () {};

// 物件的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭頭函式
const foo = async () => {};

async函式內部return語句返回的值,會成為then方法回撥函式的引數。

只有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。

任何一個await語句後面的 Promise 物件變為reject狀態,那麼整個async函式都會中斷執行。

我們希望即使前一個非同步操作失敗,也不要中斷後面的非同步操作。這時可以將第一個await放在try...catch結構裡面,這樣不管這個非同步操作是否成功,第二個await都會執行。

多個await命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發。

let foo = await getFoo();
let bar = await getBar();
//上面程式碼只有getFoo完成以後,才會執行getBar,完全可以讓它們同時觸發。

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
//上面兩種寫法,getFoo和getBar都是同時觸發,這樣就會縮短程式的執行時間。

 假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。如果當中有一個動畫出錯,就不再往下執行,返回上一個成功執行的動畫的返回值。

async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    /* 忽略錯誤,繼續執行 */
  }
  return ret;
}

按順序完成非同步操作,依次遠端讀取一組 URL,然後按照讀取的順序輸出結果。

//該方式為同步執行
//只有前一個 URL 返回結果,才會去讀取下一個 URL
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

//雖然map方法的引數是async函式,但它是併發執行的,因為只有async函式內部是繼發執行,外部不受影響。
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);
  }
}

Iterator 介面是一種資料遍歷的協議,他的next方法必須是同步的,只要呼叫就必須立刻返回值。

非同步遍歷器asyncIterator的最大的語法特點,就是呼叫遍歷器的next方法,返回的是一個 Promise 物件。物件的非同步遍歷器介面,部署在Symbol.asyncIterator屬性上面。

非同步遍歷器的next方法是可以連續呼叫的,不必等到上一步產生的 Promise 物件resolve以後再呼叫。這種情況下,next方法會累積起來,自動按照每一步的順序執行下去。

下面是非同步遍歷器的兩種使用方式

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 }
}

for...of迴圈用於遍歷同步的 Iterator 介面。新引入的for await...of迴圈,則是用於遍歷非同步的 Iterator 介面。

//createRejectingIterable()返回一個擁有非同步遍歷器介面的物件
//如果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);
  }
}

就像 Generator 函式返回一個同步遍歷器物件一樣,非同步 Generator 函式的作用,是返回一個非同步遍歷器物件。

// 同步 Generator 函式
function* map(iterable, func) {
  const iter = iterable[Symbol.iterator]();
  while (true) {
    const {value, done} = iter.next();
    if (done) break;
    yield func(value);
  }
}

// 非同步 Generator 函式
async function* map(iterable, func) {
  const iter = iterable[Symbol.asyncIterator]();
  while (true) {
    const {value, done} = await iter.next();
    if (done) break;
    yield func(value);
  }
}

普通的 async 函式返回的是一個 Promise 物件,而非同步 Generator 函式返回的是一個非同步 Iterator 物件。可以這樣理解,async 函式和非同步 Generator 函式,是封裝非同步操作的兩種方法,都用來達到同一種目的。區別在於,前者自帶執行器,後者通過for await...of執行,或者自己編寫執行器

//一個非同步 Generator 函式的執行器
async function takeAsync(asyncIterable, count = Infinity) {
  const result = [];
  const iterator = asyncIterable[Symbol.asyncIterator]();
  while (result.length < count) {
    const {value, done} = await iterator.next();
    if (done) break;
    result.push(value);
  }
  return result;
}

//使用例項
async function f() {
  async function* gen() {
    yield 'a';
    yield 'b';
    yield 'c';
  }

  return await takeAsync(gen());
}

f().then(function (result) {
  console.log(result); // ['a', 'b', 'c']
})

非同步 Generator 函數出現以後,JavaScript 就有了四種函式形式:普通函式、async 函式、Generator 函式和非同步 Generator 函式。請注意區分每種函式的不同之處。基本上,如果是一系列按照順序執行的非同步操作(比如讀取檔案,然後寫入新內容,再存入硬碟),可以使用 async 函式;如果是一系列產生相同資料結構的非同步操作(比如一行一行讀取檔案),可以使用非同步 Generator 函式。

yield*語句也可以跟一個非同步遍歷器,for await...of迴圈會展開yield*