Iterator
它是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作。
Iterator 的遍歷過程:
- 建立一個指標物件,指向當前資料結構的起始位置。
- 不斷呼叫指標物件的
next
方法
每一次呼叫next
方法,都會返回資料結構的當前成員的資訊。(返回一個包含value和done兩個屬性的物件。其中,value屬性是當前成員的值,done屬性是一個布林值,表示遍歷是否結束。)
ES6 規定,一個資料結構只要具有Symbol.iterator
屬性,就可以認為是“可遍歷的”。Symbol.iterator
屬性本身是一個函式,就是當前資料結構預設的遍歷器生成函式。(Symbol)
Symbol.iterator
,它是一個表示式,返回Symbol
物件的iterator
屬性,這是一個預定義好的、型別為Symbol
的特殊值。
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
}
class LinkedList {
[Symbol.iterator] () {
return {
next () {
return { value: 1, done: true }
}
}
}
}
複製程式碼
原生資料結構部署了遍歷器介面有:Array Map Set String TypedArray 函式的 arguments 物件 NodeList 物件
。
for...of
只要部署了遍歷器介面就可以使用for...of
遍歷。
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
}
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
複製程式碼
以下場景會呼叫 Iterator 介面
let [first, ...rest] = [1,2,3] // 解構賦值
[...'hi'] // 擴充套件運算子
let generator = function* () {
yield* [1,2,3] // yield*
};
// for...of
// Array.from()
// Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
// Promise.all() Promise.race()
複製程式碼
遍歷器返回物件除了next
方法還有return
和throw
方法,它們是可選的。
return
方法的使用場合是,如果for...o
f迴圈提前退出(通常是因為出錯,或者有break
語句),就會呼叫return
方法。return
方法必須返回一個物件。
Generator
Generator 函式是 ES6 提供的一種非同步程式設計解決方案,執行 Generator 函式會返回一個遍歷器物件。
Generator 函式使用function*
定義(有沒有空格都行),內部可以使用yield
和yield*
表示式。
function* g() {
yield 1
yield 2
return 3
yield 4
}
let a = g()
// 呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,遍歷器物件
console.log(a)
/*
__proto__: Generator
__proto__: Generator
constructor: GeneratorFunction {prototype: Generator, constructor: ƒ, Symbol(Symbol.toStringTag): "GeneratorFunction"}
next: ƒ next()
return: ƒ return()
throw: ƒ throw()
Symbol(Symbol.toStringTag): "Generator"
__proto__: Object
[[GeneratorLocation]]: VM89:1
[[GeneratorStatus]]: "suspended"
[[GeneratorFunction]]: ƒ* g()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[2]
*/
// 必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。
a.next() // { value: 1, done: false }
a.next() // { value: 2, done: false }
a.next() // { value: 3, done: true }
a.next() // { value: undefined, done: true }
複製程式碼
Generator 函式是分段執行的,yield
表示式是暫停執行的標記,而next
方法可以恢復執行,當碰到yield
就返回yield
後面的值和done
為false
,如果遇到return
就返回return
後的值和done
為true
的物件,如果沒有碰到yield
和return
則返回值為undefined
和done
為true
的物件。
yield表示式後面的表示式,只有當呼叫next方法、內部指標指向該語句時才會執行,因此等於為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。
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
}
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
複製程式碼
任意一個物件的Symbol.iterator
方法,等於該物件的遍歷器生成函式,呼叫該函式會返回該物件的一個遍歷器物件,可以把 Generator 賦值給物件的Symbol.iterator
屬性,從而使得該物件具有 Iterator 介面。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
複製程式碼
yield
可以有返回值,它通過next
方法傳入。
function* g() {
let v1 = yield
console.log(v1)
let v2 = yield 2
console.log(v2)
}
let a = g()
a.next() // { value: undefined, done: false }
a.next([1,2,3]) // { value: 2, done: false }
a.next() // { value: undefined, done: true }
// [1,2,3]
// undefined
複製程式碼
上面的例子,第一個next
方法執行到第一個yield
暫停,執行第二次next
方法時,我們用[1,2,3]
做為引數,這時第一個yield
就返回[1,2,3]
,所以第一次列印[1,2,3]
第二次列印undefined
。
所以對第一個next
方法傳遞引數是沒有用的,第二個next
的引數才作為第一個yield
的返回值。
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
複製程式碼
只要done
為true
,for...of
就會終止迴圈,所以return
的返回值沒有列印。
Generator 函式原型上有throw
方法,可以用來在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a'); // throw 方法可以接受一個引數,該引數會被catch語句接收
i.throw('b'); // throw 方法會附帶執行一次 next 方法
} catch (e) {
console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
複製程式碼
如果內部沒有捕獲,那麼錯誤就會跑到外面來,被外部捕獲。如果內外都沒有捕獲錯誤,那麼程式將報錯,直接中斷執行。
在使用throw
之前,必須執行一次next
方法,否則丟擲的錯誤不會被內部捕獲,而是直接在外部丟擲,導致程式出錯。
函式體內的錯誤可以被外部捕獲。
function* g() {
yield 1
yield 2
throw new Error('err')
yield 3
}
let a = g()
try {
console.log(a.next()) // { value: 1, done: tfalse }
console.log(a.next()) // { value: 2, done: false }
console.log(a.next()) // 報錯
} catch(e) {}
console.log(a.next()) // { value: undefined, done: true }
複製程式碼
只要內部錯誤沒有捕獲,跑到外面來,那麼迭代器就會自動停止。
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 }
// ---------------
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 }
// 如果 Generator 函式內部有try...finally程式碼塊,且正在執行try程式碼塊,那麼return方法會推遲到finally程式碼塊執行完再執行
複製程式碼
yield*
表示式,用來在一個 Generator 函式內部,呼叫另一個 Generator 函式。
function* bar() {
yield 'x';
yield* foo();
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"
function* g() {
yield 1
yield 2
return 3
}
function* e() {
let returnValue = yield* g()
console.log(returnValue) // 3
// 如果另一個函式帶有 return 則需要自己獲取
yield* ['a', 'b', 'c'] // 還可以是 陣列,字串這些原生帶有迭代器的物件
}
複製程式碼
如果yield
表示式後面跟的是一個遍歷器物件,需要在yield
表示式後面加上星號,表明它返回的是一個遍歷器物件。
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
[...iterTree(tree)]
複製程式碼
yield*
可以輕鬆的抹平陣列。
Generator 函式不能和new
命令一起用。
Async
ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。它可以非常清晰的將非同步操作寫成同步操作。
let p = async function getData() {
let data = await db.getData()
console.log(data)
}
console.log(p)
/*
Promise {<resolved>: undefined}
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: undefined
*/
let b = async function () {} // 函式表示式
let c = async () => {} // 箭頭函式
複製程式碼
async
函式 以async
開頭,其內部可以使用await
等待非同步操作完成。async
函式會返回一個Promise
物件,所以可以使用then方法新增回撥函式。
函式執行的時候,一旦遇到await
就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。
async
函式內部return
語句返回的值,會成為then
方法回撥函式的引數,async
函式內部丟擲錯誤,會導致返回的 Promise
物件變為reject
狀態。丟擲的錯誤物件會被catch
方法回撥函式接收到。
async function a() {
throw new Error('err')
}
a() // Uncaught (in promise) Error 報錯,但不會終止程式
console.log(123) // 正常執行
複製程式碼
async
函式返回的Promise
物件,必須等到內部所有await
命令後面的 Promise 物件執行完,才會發生狀態改變,除非遇到return
語句或者丟擲錯誤。
await
命令後面一般是一個 Promise 物件,返回該物件的結果。如果不是 Promise 物件,就直接返回對應的值。await
命令後面是一個thenable
物件(即定義then方法的物件),那麼await
會將其等同於 Promise 物件。- 任何一個
await
語句後面的 Promise 物件變為reject狀態,那麼整個async函式都會中斷執行。
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);
}
} // 我們可以將多個 await 命令放入 try-catch 中
複製程式碼
async
函式其實它就是 Generator 函式的語法糖。
async function fn(args) {
// ...
}
// 等同於
function fn(args) {
return spawn(function* () {
// ...
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) { // 返回一個 Promise 物件
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e); // 執行 fn 中的程式碼,如果出錯直接 reject 返回的 Promise
}
if(next.done) { // 如果 fn 執行完成則 resolve fn return 的值
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
// 將 yield 返回的值變成 Promise 並執行它的 then 方法
step(function() { return gen.next(v); });
// 當 yield 後面的 Promise 執行成功完成時則繼續執行 fn 函式
// 並將它產生的值傳入 fn 函式
}, function(e) {
step(function() { return gen.throw(e); });
// 如果出現錯誤則將錯誤傳入 fn 內部。
// 如果內部沒有捕獲則被本函式上面的 try-catch 捕獲
});
}
step(function() { return gen.next(undefined); });
});
}
複製程式碼
非同步遍歷器
非同步遍歷器和遍歷器的區別在於,非同步遍歷器返回的是一個 Promise 物件,但是它的值的格式和遍歷器一樣。非同步遍歷器介面,部署在Symbol.asyncIterator
屬性上面。
非同步遍歷器它的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
複製程式碼
for await...of
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
捕捉。
它也可以用於同步遍歷器。
非同步 Generator 函式
就像 Generator 函式返回一個同步遍歷器物件一樣,非同步 Generator 函式的作用,是返回一個非同步遍歷器物件。
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
// 同步 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);
}
}
複製程式碼
Generator 函式處理同步操作和非同步操作時,能夠使用同一套介面。非同步 Generator 函式內部,能夠同時使用await
和yield
命令。
如果非同步 Generator 函式丟擲錯誤,會導致 Promise 物件的狀態變為reject
,然後丟擲的錯誤被catch
方法捕獲。
yield*
yield*語句也可以跟一個非同步遍歷器。
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最終會等於 2
const result = yield* gen1();
}
for await (const x of gen2()) {
console.log(x);
}
// a
// b
複製程式碼