你可能不知道的迭代器與生成器

原文發佈於 github.com/ta7sudan/no…, 如需轉載請保留原做者 @ta7sudan.javascript

注: 本文只會寫一些我的以爲比較重要的細節, 而非全面介紹迭代器和生成器.java

迭代器, 迭代器協議和可迭代協議

咱們知道, js 中並無其餘語言那樣的接口語法來強制約束一個對象必須實現某些方法, 好比 Java 的 interface. 不過語法只是形式, 接口的思想在任何語言裏都是適用的. 在 js 裏要想實現接口每每是靠着口頭的約定, 固然這種約定是不具備語法層面的約束力的. 而衆所周知的約定, 咱們也能夠稱它爲協議. ES6 定義了迭代器協議就是這樣一種約定, 本質上來講也就是定義了一個接口.git

迭代器協議

迭代器協議規定了一個對象須要實現一個 next() 方法, 該方法接受一個可選參數, 方法返回值必須是一個對象, 對象必須包含 donevalue 兩個屬性, 其中 done 是一個布爾值, value 爲類型任意, 當 donetrue 時, value 能夠省略. 其實到這裏, 若是實現了上面全部內容, 則一個對象就算是實現了迭代器協議了. 固然, 規範有一些語義層面的約束, 那就是 donetrue 時, 意味着迭代已經完成, 迭代器不會再產生新的值, 而爲 false 則表示可迭代序列還能夠繼續產生新的值. 總的來講, 語義層面的約束你也能夠不遵照它, 最多隻會產生邏輯錯誤, 而前面的約束不遵照, 則至關於沒有實現迭代器協議, 在使用的時候會產生運行時錯誤.github

迭代器

咱們把一個實現了迭代器協議的對象稱爲迭代器或迭代器對象. 咱們通常稱迭代器關閉了/結束了便是指 next() 返回值的 donetrue 了. 一個迭代器就像下面這樣.數組

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	}
};
複製代碼

很是簡單, 就一個普通對象, 並無任何特殊的地方, 這就是一個實現了迭代器協議的對象.bash

基於迭代器協議的特色, 咱們能夠知道, 一個迭代器對象的狀態, 一旦 next() 返回的對象的 donetrue, 以後再調用 next() 就不可能再回到 donefalse 的狀態了. 固然, 從實現的角度來講你也能夠違反這一點, 可是這並無什麼好處. 基於這一點, 最好不要在一個迭代器關閉了以後重用這個迭代器對象.ide

可迭代協議

可迭代協議規定了一個對象須要實現一個屬性名爲 Symbol.iterator 的方法, 方法不接受參數, 返回值必須是一個對象, 對象必須實現了迭代器協議, 即該方法返回一個迭代器. 這個方法通常也被稱爲 @@iterator 方法.函數

可迭代對象

可迭代對象即實現了可迭代協議的對象, 一個簡單的可迭代對象就像下面這樣.ui

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}
複製代碼

iterable 即一個可迭代對象, 可迭代對象能夠被用於 for...of, 展開運算符 ..., 數組解構和 yield*.this

一樣, 基於迭代器協議的特色, @@iterator 方法最好每次調用都返回一個新的迭代器對象. 雖然可迭代協議並無約束這一點.

生成器

生成器是一個函數, 返回一個對象, 對象實現了迭代器協議和可迭代協議.

function* test() {
	yield 1;
	yield 2;
	yield 3;
}

console.log(typeof test); // function
console.log(typeof test().next); // function
console.log(typeof test()[Symbol.iterator]); // function
複製代碼

一般咱們把生成器函數的返回值稱爲生成器對象. 雖然聲明語法看上去不太同樣, 不過它的類型也就是一個普通函數.

生成器不能做爲構造函數, 由於它沒有 [[Construct]] 內部屬性.

生成器也不存在箭頭函數形式的匿名生成器. eg.

var test = *() => {
	yield 1;
}; // error

var test = function* () {
	yield 1;
}; // ok
複製代碼

生成器做爲屬性方法的簡寫能夠這樣.

var obj = {
	*test() {
		yield 1;
	}
};
// 而不用
var obj = {
	test: function* {
		yield 1;
	}
};
// 雖然這樣也OK
複製代碼

調用生成器函數並不執行生成器函數, 而是返回生成器對象, 只有調用生成器對象的 next() throw() return() 方法纔會執行生成器函數.

OK, 如今有了生成器函數以後, 咱們要實現一個可迭代對象就更加簡單了, 能夠這樣寫.

var iterable = {
	*[Symbol.iterator]() {
		yield 1;
		yield 2;
		yield 3;
	}
};
複製代碼

比起前面本身手寫一個迭代器, 再手寫可迭代對象的 Symbol.iterator 方法又簡潔了許多.

yield

yield 後面能夠跟一個表達式, 而它和後面的表達式自己也是一個表達式, 因此能夠出如今任何表達式能夠出現的位置.

function* test() {
	var a = yield 1;
	return a;
}
var iter = test();
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: undefined, done: true}
複製代碼

yield 表達式的值是 iter.next() 傳入的值, 也就是說, yield 表達式的值默認是 undefined. 這裏咱們沒有給 next() 傳入值, 因此 a 也是 undefined.

咱們能夠簡單地認爲, 生成器函數的執行在 yield 表達式位置暫停, 而後下一次執行從 yield 表達式所在的語句(包括)開始.

function* test() {
	var a = console.log('aaa') + (yield 1) + console.log('bbb');
	return a;
}
var iter = test();
console.log(iter.next());
console.log(iter.next());
// {value: 1, done: false}
// test
// {value: NaN, done: true}
複製代碼

能夠看到, 這裏一開始執行了 console.log('aaa'), 由於 + 從左往右依次求值, 這個會在 yield 表達式以前被執行, 然後面的 console.log('bbb') 則不會在第一次 next() 時執行, 由於 yield 表達式已經暫停了函數執行, 因此具體函數在哪一個位置暫停, 要看 yield 表達式出現的位置, 以及一些運算符的執行順序. 好比若是 yield 出如今逗號表達式的後面的某一項, 則逗號表達式前面的表達式都會在 yield 暫停以前被執行, 而若是 yield 表達式出如今逗號表達式前面的某一項, 則相反.

function* test() {
	(yield 1, console.log('test'));
}
var iter = test();
console.log(iter.next());
console.log(iter.next());
// {value: 1, done: false}
// test
// {value: undefined, done: true}

function* test() {
	(console.log('test'), yield 1);
}
var iter = test();
console.log(iter.next());
console.log(iter.next());
// test
// {value: 1, done: false}
// {value: undefined, done: true}
複製代碼

yield 其實更像是一個運算符, 它的優先級比較低.

function* test() {
	var a = yield 1 + 2;
}
var iter = test();
console.log(iter.next());
console.log(iter.next());
// {value: 3, done: false}
// {value: undefined, done: true}
複製代碼

能夠看到, 這裏第一次 next() 的返回值是 3 而不是 1, 由於先計算了 1 + 2, 這至關於 yield (1 + 2). 若是須要 yield 的值是 1, 則應該加上括號.

function* test() {
	var a = (yield 1) + 2;
}
var iter = test();
console.log(iter.next());
console.log(iter.next());
複製代碼

yield 不能跨越函數的邊界, 就像 return 同樣. 因此這樣是不行的.

function* test() {
	var arr = [1, 2, 3];
	arr.forEach(item => {
		yield item;
	});
}
複製代碼

生成器對象

前面咱們已經說過, 生成器對象實現了迭代器協議和可迭代協議, 因此毫無疑問它具備 next() 方法和 Symbol.iterator 方法, 因此生成器對象既是可迭代對象又是迭代器對象. 事實上, 它主要有如下幾個方法.

  • next()
  • throw()
  • return()
  • [Symbol.iterator]()

前面三個函數的返回值都同樣, 都是一個具備 donevalue 的對象.

next()

next() 方法很簡單, 就如同迭代器協議中所說的, 它返回值必定是一個對象, 且對象必定包含了 donevalue 兩個屬性, 只不過它還多了一個可選參數, 參數做爲上一個 yield 表達式的值. 另外一方面, 一旦 next() 被調用, 則生成器函數從上一個 yield 表達式位置或函數起始位置恢復執行, 執行到下一個 yield 表達式的位置暫停, 它的參數做爲上一個 yield 表達式的值, 返回值始終非空, 而且返回值的 value 是下一個 yield 表達式的給出的值(便是 yield 後面表達式的值, 而不是 yield 表達式的值). 注意關鍵詞執行到, 上一個, 下一個, 因此在第一次執行 next() 時, 給它傳參是沒有意義的, 由於第一次執行 next() 是執行到第一個 yield, 而它沒有上一個 yield 表達式. 事實上, 第一次傳參是經過生成器函數自己的調用來完成的.

function* test() {
	var a = yield 1;
	return a;
}
var iter = test();
console.log(iter.next(2)); // 沒有任何做用
console.log(iter.next());

// ----------

function* test() {
	var a = yield 1;
	return a;
}
var iter = test();
console.log(iter.next());
console.log(iter.next(2)); // 有用, 使得 a == 2
複製代碼

另外一方面, 當咱們調用生成器函數並給它傳參的時候, 並不會執行生成器函數自己.

function* test(b) {
	console.log(b)
	var a = yield 1;
	return a;
}
var iter = test(0); // 這裏不會執行 console.log(b)
console.log(iter.next());
console.log(iter.next());
複製代碼

而是在第一次執行 next() 的時候纔開始執行生成器函數. 換句話說, next() 能夠啓動生成器函數. 從這一點來講, 生成器函數也具備收集參數延遲執行的做用.

最後, 當生成器函數運行結束之後, 屢次調用生成器對象的 next() 不會再讓生成器函數從新開始執行或繼續執行了, 而且始終返回 {done: true, value: undefined}.

throw()

throw()next() 很類似, 一樣也接受一個參數, 返回值也是一個包含 donevalue 的對象. throw() 被執行, 生成器函數從上一個 yield 表達式位置或函數起始位置恢復執行, 執行到下一個 yield 表達式位置暫停, 可是一旦 throw() 使得生成器函數開始執行, 就會在生成器函數內部拋出一個異常, 它的參數被做爲異常的值, 它的返回值的 value 是下一個 yield 表達式給出的值. 什麼意思呢?

function* test(b) {
	try {
		yield 1;
	} catch (error) {
		console.log('catch');
	}
	yield 2;
}
var iter = test();
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.throw(new Error('test')));
// catch
// {value: 2, done: false}
複製代碼

這至關於

function* test(b) {
	try {
		yield 1;
		throw new Error('test');
	} catch (error) {
		console.log('catch');
	}
	yield 2;
}
複製代碼

這給了咱們從外部向生成器函數中拋出異常的能力, 而且這個異常能夠在生成器函數內部被捕獲到. 另外, 它的參數類型並不必定要是 Error 類型, 能夠是任意類型, 它們都會被看成異常從而被捕獲.

最後, 當生成器函數運行結束之後, 再調用生成器對象的throw() 的行爲和 next() 幾乎同樣, 只不過它仍是會觸發一個異常.

return()

一樣, return()next() 也相似, 它也接受一個參數, 而且返回一個包含 donevalue 的對象, return() 也會使得生成器函數從上一個 yield 表達式位置或函數起始位置恢復執行, 執行到下一個 yield 表達式位置暫停, 可是一旦 return() 使得生成器函數開始執行, 它就會觸發生成器函數直接 return. 若是沒有下一個可達的 yield 表達式, 則它的參數就是生成器函數 return 的值, 它的返回值的 value 就是 return 的值也即它的參數, 而 done 則始終爲 true. 注意這裏可能和不少人認知不太同樣, 由於大部分時候它就直接觸發生成器函數返回了, 生成器函數後面的內容都不會被執行了, 你怎麼說它能使生成器函數恢復執行呢? 而且它後面的 yield 表達式怎麼可能還有機會執行呢? 注意咱們強調了可達的, 別忘了, 咱們還有超越 returnfinally.

function* test() {
	try {
		yield 1;
		yield 2;
		yield 3;
	} catch(e) {

	} finally {
		console.log('ok');
	}
}
var iter = test();
console.log(iter.next()); // {value: 1, done: false}
// ok
console.log(iter.return(5)); // {value: 5, done: true}
複製代碼

能夠看到, 它實際上是觸發了生成器函數的執行的, 若是真的不觸發生成器函數執行, 那就不會輸出 finally 中的 ok 了, 它就像函數的 return 同樣, 最終仍是要先等待 finally 的執行, 因此是先輸出了 ok 再輸出了 return() 的值, 而且 return()done 置爲了 true.

當生成器函數執行完之後, 屢次調用生成器對象的 return() 行爲也和 next() 差很少, 只不過它的返回值 value 便是它的參數.

再看一個例子.

function* test() {
	try {
		yield 1;
		yield 2;
		yield 3;
	} catch(e) {

	} finally {
		yield 4;
		console.log('ok');
	}
}
var iter = test();
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.return(5)); // {value: 4, done: false}
// ok
console.log(iter.next()); // // {value: 5, done: true}
複製代碼

這個例子就更加詭異了, return() 返回值的 done 再也不是本身的參數了, 它的 done 也再也不是 true 了, 因此 return() 返回值的 done 並不必定老是爲 true, value 也不必定老是它本身的參數. 可是這事情要怎麼理解呢? 語言表述能力有限, 比較難說清楚, 可是咱們能夠作一個等價替換.

function* test() {
	try {
		yield 1;
		return 5;
		yield 2;
		yield 3;
	} catch(e) {

	} finally {
		yield 4;
		console.log('ok');
	}
}
var iter = test();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
複製代碼

結果和上面的例子同樣, 而其實這也是出現這樣結果的緣由.

其實我的以爲, 對於 next() throw()return(), 咱們都把它看成是 next() 就行了, 而對於 throw()return(), 就當它是等價的 throw 語句和 return 語句, 而後咱們再按照 next() 的邏輯走. 這樣就不會有什麼理解上的誤差了. 固然, 上面的例子只是極端狀況, 實際上咱們幾乎不會也不該當這麼寫就是了.

另外, 上面的 return() 都是基於生成器對象來講明的, 可是並不只僅只有生成器對象具備 return() 方法, return() 方法也不只僅只對生成器對象有意義, 以後會更具體討論.

yield*

yield* 後面能夠接一個表達式, 表達式的值必須是一個可迭代對象, yield* 會調用可迭代對象的 Symbol.iterator 方法. 而 yield* 自己也是一個表達式, 即

<expr> := yield* <expr>
複製代碼

不少地方都說 yield* 是委託生成器的, 其實 yield* 並不只僅能夠委託生成器, 而是能夠委託任意可迭代對象.

yield* generator(); // ok
yield* [1, 2, 3]; // ok

// or

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	},
	return() {
		console.log('return');
		return {done: true};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

function* test() {
	var a = yield* iterable;
	return a;
}
var it = test();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
複製代碼

以上這些都是 OK 的, 因此 yield* 後面不只僅能夠是生成器對象. 注意咱們上面的例子中變量 a 的值, 也即 yield* 表達式的值. yield* 表達式的值是可迭代對象的最後一個值, 也即 donetruevalue 的值.

咱們還注意到, 咱們的迭代器中有個 return() 方法, 儘管在這裏是沒有什麼意義的, 不過以後的例子會和它進行對照. 這裏只說一下, yield* 不會調用迭代器的 return() 方法, 由於 yield* 被視爲消費完了可迭代對象(消費完是指 donetrue), 注意這個方法是在迭代器上, 不是在可迭代對象上. 注意這裏咱們強調了消費完這一律念, 在後面的數組解構例子中會更清楚看到這一點.

基於上面的知識, 咱們須要注意一點, 就是在委託生成器的時候, 默認狀況下是不會 yield 出生成器的返回值的.

function* f() {
	yield 1;
	yield 2;
	yield 3;
	console.log('test');
	return 4;
}

function* test() {
	yield* f();
}
var it = test();
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: 3, done: false}
// test
console.log(it.next()); // {value: undefined, done: false}
複製代碼

能夠看到, 並無 f() 的返回值 4, 可是它仍是會執行完咱們委託的生成器函數, 若是但願 yield 這個返回值, 咱們應當像前面那樣去取得 yield* 表達式的值, 再加一個 yield. 即

function* f() {
	yield 1;
	yield 2;
	yield 3;
	return 4;
}

function* test() {
	yield yield* f();
}
var it = test();
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: 3, done: false}
console.log(it.next()); // {value: 4, done: false}
複製代碼

若是一個可迭代對象能夠產生 n 個值, 則 yield* 只能 yield 出前 n - 1 個值, 而最後一個值做爲 yield* 表達式自己的返回值.

for...of

for...of 用來迭代一個可迭代對象, 它會調用可迭代對象的 Symbol.iterator 方法獲得一個迭代器, 循環每執行一次就會調用一次迭代器對象的 next() 方法, 並將 next() 返回的對象的 value 存儲在一個變量中, 循環持續這一過程知道返回對象的 donetrue, 固然, donetrue 時的 value 也會被遍歷到.

若是將 for...of 用於不可迭代的對象則報錯.

for...of 遍歷字符串時獲得的是完整的字符, 而非單個編碼單元, 即咱們能夠放心使用 for...of 來獲取字符串中的每一個字符.

for...ofdonetrue 時, 就中止讀取其餘值, 即若是一個可迭代對象能夠產生 n 個值, 則 for...of 只能遍歷前 n - 1 個值.

function* gen() {
	yield 1;
	yield 2;
	yield 3;
	console.log('test');
	return 4;
}

for (const v of gen()) {
	console.log(v);
}
// 1
// 2
// 3
// test
複製代碼

能夠看到, 並不會輸出 4. 可是一樣, 它也會執行完生成器函數.

break

還記得咱們以前例子中的迭代器對象有個 return() 方法嗎? 還記得以前咱們說過, return() 方法不只僅是生成器對象特有的, 它也不只僅只對生成器對象有意義. 事實上, 儘管迭代器協議沒有要求實現一個 return() 方法, 但這個方法對於迭代器對象而言也很重要.

for...of 中, 一旦循環被 break, 則會調用可迭代對象的迭代器的 return() 方法. 仍是以前的例子.

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	},
	return() {
		console.log('return');
		return {
			value: 7,
			done: false
		};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

for (const v of iterable) {
	console.log(v);
	break;
}
// 0
// return
複製代碼

能夠看到, 咱們的 return() 方法被調用了, 這有什麼用呢? 這讓咱們實現的迭代器可以知道本身何時被提早關閉了.

for...of 在遍歷的時候老是會調用這個迭代器的 return() 方法嗎? 並非.

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	},
	return() {
		console.log('return');
		return {
			value: 7,
			done: false
		};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

for (const v of iterable) {
	console.log(v);
}
// 0
// 1
// 2
// 3
// 4
複製代碼

這裏咱們沒有使用 break, 因此 return() 也沒有被調用. 事實上, 只有當一個可迭代對象產生的數據沒有被消費完時纔會調用可迭代對象的迭代器的 return(). 怎麼定義消費完? 準確來講應該是, donetrue 時, 而且第 n - 1 次迭代所有完成就算消費完. 能夠看下面的例子.

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	},
	return() {
		console.log('return');
		return {
			value: 7,
			done: false
		};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

for (const v of iterable) { // 沒有消費完
	console.log(v);
	if (v === 2) {
		break;
	}
}

// or
for (const v of iterable) { // 也沒有消費完!!!
	console.log(v);
	if (v === 4) {
		break;
	}
}
// or
for (const v of iterable) { // 也沒有消費完!!!
	console.log(v);
	throw new Error('err');
}
// or
for (const v of iterable) { // 消費完了
	console.log(v);
}
複製代碼

能夠看到, 前面三種狀況都是沒有消費完的, 即便已經讀到 n - 1 個數據了, 可是由於該次迭代還未執行完就 break 了, 因此也沒有消費完, 或者你也能夠理解爲, 只要執行了 break 就沒有消費完, 另外在某一次迭代中由於異常中斷了也屬於沒有消費完.

即咱們最後能夠總結爲, for...of 會在可迭代對象的數據沒有被消費完時調用可迭代對象的迭代器的 return() 方法.

那麼這個 return() 方法有什麼要求沒呢? 它不接受參數(固然, 生成器對象的能夠接受一個可選參數), 它必須返回一個對象, 不然被調用時會報錯. 只要返回的對象須要包含什麼其實並不重要. 只不過一般來講, 咱們也按照 next() 同樣返回一個包含 done: true 的對象, 至於 value 是否須要都沒啥關係, 不會被用到. 另外一方面是, 建議在 return() 裏面也修改掉 next() 返回對象的 donetrue, 確保邏輯上這個迭代器已經結束.

var iter = {
	i: 0,
	done: false,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: this.done
			};
		} else {
			this.done = true;
			return {
				value: this.i,
				done: this.done
			};
		}
	},
	return() {
		console.log('return');
		this.done = true;
		return {
			value: 7,
			done: this.done
		};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

for (const v of iterable) {
	console.log(v);
	break;
}

for (const vv of iterable) {
	console.log(vv);
}
複製代碼

這裏迭代器的全部方法返回的 done 都共享了一個內部狀態, 這樣第二個 for...of 就不會開始迭代了, 不然的話第二個 for...of 又會接着遍歷迭代器.

另外, 前面的例子種一直有個問題, 就是咱們的迭代器被重用了, 但這裏只是爲了方便演示, 實際狀況中咱們毫不應該這麼寫, 這裏引用 MDN 的例子.

var gen = (function *(){
  yield 1;
  yield 2;
  yield 3;
})();
for (let o of gen) {
  console.log(o);
  break;  // Closes iterator
}

// The generator should not be re-used, the following does not make sense!
for (let o of gen) {
  console.log(o); // Never called.
}
複製代碼

因此記住不要重用迭代器!

展開運算符

展開運算符接受的也是一個可迭代對象. 因此把一個可迭代對象轉爲數組的最簡單方式是這樣.

var arr = [...iterable];
複製代碼

固然, 下面這些也都是合法的.

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	},
	return() {
		console.log('return');
		return {done: true};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

var a = [...iterable];

// or

function* gen() {
	yield 1;
	yield 2;
	yield 3;
}

var b = [...gen()];
複製代碼

另外, 咱們發現, 展開運算符不會調用可迭代對象的 Symbol.iterator 返回的迭代器的 return() 方法, 由於展開運算符也會消費完可迭代對象產生的值.

可是展開運算符和 for...of yield* 同樣, 也會忽略掉最後一個值, 只要 donetrue 就中止讀取其餘值.

function* gen() {
	yield 1;
	yield 2;
	yield 3;
	console.log('test');
	return 4;
}

var arr = [...gen()];
// test
console.log(arr); // [1, 2, 3]
複製代碼

一樣, 它也會執行完生成器函數.

數組解構

數組解構其實也並不要求是對一個數組進行解構賦值, 而是對任何可迭代對象均可以進行數組解構, 因此下面這些也都是合法的.

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	},
	return() {
		console.log('return');
		return {done: true};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}


var [a, b, c] = iterable;

// or
function* gen() {
	yield 1;
	yield 2;
	yield 3;
}

var [a, b, c] = gen();
console.log(a, b, c);
複製代碼

數組解構在沒有將可迭代對象的值消費完時, 會調用可迭代對象的 Symbol.iterator 返回的迭代器的 return() 方法, 而若是數組解構消費完了可迭代對象時(donetrue 時), 則不會調用 return() 方法. 能夠看下面的例子.

var iter = {
	i: 0,
	next() {
		if (this.i < 5) {
			return {
				value: this.i++,
				done: false
			};
		} else {
			return {
				value: this.i,
				done: true
			};
		}
	},
	return() {
		console.log('return');
		return {
			done: true
		}
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

var [a, b, c] = iterable;
// return
複製代碼

而若是最後是這樣

var [a, b, c, d, e, f] = iterable;
複製代碼

則不會調用 return(), 由於可迭代對象已經被消費完了.

一樣, 數組解構也不會讀取最後一個值, 也是在 donetrue 時中止讀取.

function* gen() {
	yield 1;
	yield 2;
	yield 3;
	console.log('test');
	return 4;
}

var [a, b, c, d] = gen();
// test
console.log(a, b, c, d); // 1 2 3 undefined
複製代碼

一樣, 在這種時候, 它也會執行完生成器函數.

GeneratorFunction

咱們知道全部的普通函數都是 Function 的實例, 咱們也能夠用 new Function() 來建立一個函數, 可是若是是生成器函數, 是否也有這樣的形式建立? 看着這個標題, 可能你會認爲存在一個 GeneratorFunction 的全局對象, 然而其實全局做用域中並不存在 GeneratorFunction 這麼一個內建對象. 不過僅僅是說, 它不在全局做用域中而已, 這個內建對象自己仍是存在的. 咱們能夠經過下面這樣的方式獲取它.

var GeneratorFunction = Object.getPrototypeOf(function*(){}).constructor
複製代碼

以後咱們即可以使它來建立生成器函數了.

var test = new GeneratorFunction('arg0', 'yield 1');
複製代碼

總的來講, 它和 Function 幾乎同樣, 好比它建立的生成器函數也是在全局做用域的. 具體用法參考 MDN.

資源的回收

如今咱們來看一個具體場景. 考慮咱們的迭代器是用來按行讀取文件的, 每次調用迭代器的 next() 便會返回一行的內容, 因此咱們的迭代器這樣實現.

var iter = {
	file: {
		line: 0,
		readLine() {
			return `line ${this.line++}`;
		},
		close() {
			console.log('close');
		}
	},
	done: false,
	next() {
		if (this.file.line < 5) {
			return {
				value: this.file.readLine(),
				done: this.done
			}
		} else {
			this.file.close();
			this.done = true;
			return {
				value: 'EOF',
				done: this.done
			}
		}
	}
};
複製代碼

在這裏咱們模擬了一個文件, 它至關於一個文件描述符, 而且它有一個 close() 方法. 咱們應當在讀取完全部行(假設一共 5 行)以後關閉這個文件, 因此上面的代碼看起來沒什麼問題. 接着咱們構造一個可迭代對象.

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}
複製代碼

最終咱們經過 for...of 來實現按行讀取文件.

var iter = {
	file: {
		line: 0,
		readLine() {
			return `line ${this.line++}`;
		},
		close() {
			console.log('close');
		}
	},
	done: false,
	next() {
		if (this.file.line < 5) {
			return {
				value: this.file.readLine(),
				done: this.done
			}
		} else {
			this.file.close();
			this.done = true;
			return {
				value: 'EOF',
				done: this.done
			}
		}
	},
	return() {
		this.file.close();
		this.done = true;
		return {
			done: this.done
		};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

for (const line of iterable) {
	console.log(line);
}
// line 0
// line 1
// line 2
// line 3
// line 4
// close
複製代碼

很好, 一切正常, 咱們優雅地實現了按行讀取, 而且關閉了這個文件. 那假如咱們只讀了一行就想退出 for...of 循環呢? 很簡單, 咱們加上一個 break 就好.

for (const line of iterable) {
	console.log(line);
	break;
}
// line 0
複製代碼

可是咱們發現此次文件沒有被正確關閉掉. So, 怎麼辦呢? 假如咱們做爲迭代器的實現者, 其實咱們並不知道其餘人/用戶會怎麼使用咱們的迭代器, 咱們但願最好可以有一種方式, 可以讓咱們的迭代器知道本身是否被消費完, So, 咱們很容易想到前面提到的迭代器的 return() 方法. 因而咱們這麼實現.

var iter = {
	file: {
		line: 0,
		readLine() {
			return `line ${this.line++}`;
		},
		close() {
			console.log('close');
		}
	},
	done: false,
	next() {
		if (this.file.line < 5) {
			return {
				value: this.file.readLine(),
				done: this.done
			}
		} else {
			this.file.close();
			this.done = true;
			return {
				value: 'EOF',
				done: this.done
			}
		}
	},
	return() {
		this.file.close();
		this.done = true;
		return {
			done: this.done
		};
	}
};

var iterable = {
	[Symbol.iterator]() {
		return iter;
	}
}

for (const line of iterable) {
	console.log(line);
	break;
}
// line 0
// close
複製代碼

OK, 如今咱們如願關閉了文件, 不管是否讀完了全部內容.

可是上面例子中咱們都是本身實現的迭代器對象, 那對於生成器函數返回的生成器對象呢? 毫無疑問, 咱們知道若是 for...of break 了確定也會調用生成器對象的 return() 方法, 可是這個方法並非咱們本身實現的, 難道咱們爲了作資源釋放的操做須要重寫生成器對象的 return() 方法嗎? 即便這樣能夠, 可是你可以保證你實現的 return() 的行爲和 Generator.prototype.return() 一致嗎? 仍是讓咱們來看例子吧.

function* genf() {
	var file = {
		close() {
			console.log('close');
		}
	};
	yield 'line 0';
	yield 'line 1';
	yield 'line 2';
	yield 'line 3';
	yield 'line 4';
	file.close();
}


for (const line of genf()) {
	console.log(line);
}
// line 0
// line 1
// line 2
// line 3
// line 4
// close
複製代碼

如今文件被正確關閉, 讓咱們給它加上 break.

function* genf() {
	var file = {
		close() {
			console.log('close');
		}
	};
	yield 'line 0';
	yield 'line 1';
	yield 'line 2';
	yield 'line 3';
	yield 'line 4';
	file.close();
}


for (const line of genf()) {
	console.log(line);
	break;
}
// line 0
複製代碼

GG, 並無關閉文件. 毫無疑問, 此時 return() 是會被調用的, 可是這個 return() 並不受咱們控制, 重寫 return() 也不是一個明智的操做. 咱們須要的是可以知道 return() 何時被調用了, 這樣咱們能夠在 return() 被調用以後釋放掉資源. 再仔細想一想, 咱們真的須要知道 return() 何時被調用了嗎? 其實咱們只須要在 return() 被調用以後釋放掉資源, 至於 return() 何時被調用咱們其實並不關心, 知道 return() 何時被調用只是讓咱們能夠在以後釋放資源, 可是咱們知道 return() 何時被調用並非必要條件. So, 怎麼確保在 return() 以後能執行咱們想要的操做? 優先級最高的 finally.

function* genf() {
	var file = {
		close() {
			console.log('close');
		}
	};
	try {
		yield 'line 0';
		yield 'line 1';
		yield 'line 2';
		yield 'line 3';
		yield 'line 4';
	} finally {
		file.close();
	}
}


for (const line of genf()) {
	console.log(line);
	break;
}
// line 0
// close
複製代碼

OK, 一切完美.

總結一下, 在咱們本身實現迭代器的時候, 最好加上 return() 方法, 尤爲當迭代器涉及 IO 之類的操做時, 有了 return() 方便咱們進行資源回收, 可是資源回收操做不只僅應該在 return() 中實現, 也須要在 next() 中實現, 由於 return() 並不老是會被調用, 而是隻有當迭代器沒有被消費完時纔會被調用. 另外最好確保 next()return() 調用之後的 done 的狀態一致, 即若是 return() 被調用, 則下次 next()done 也爲 true, 當 next() 調用後的 donetrue, 則 return() 返回對象的 done 也爲 true.

而在咱們實現生成器函數的時候, 若是有 IO 操做涉及一些資源的建立與回收, 也記得在最後使用 finally 進行回收.

The end

最後再比較一下 for...of yield* 數組解構和展開運算符.

  • 它們都接受一個可迭代對象, 而且都會調用可迭代對象的迭代器的 next() 方法
  • 若是一個可迭代對象能夠產生 n 個值, 則它們最多都會調用 n 次 next() 方法
  • 對於生成器函數, 它們最多都會將生成器函數執行完
  • 它們最多都只使用前 n - 1 個值
  • 對於 for...of 它只會迭代最多 n - 1 次, 可是執行 n 次 next()
  • 對於 yield*, 它必定產生 n - 1 個 yield 表達式, 執行 n 次 next(), 而第 n 個值做爲它自身表達式的值
  • 對於數組解構, 若是有 m 個變量被賦值, 則它執行 m 次 next(), 而它最多隻能使用 n - 1 個值, 因此它最多給 n - 1 個變量賦值, 此時若是是生成器函數則並不會執行完, 想要將生成器函數執行完則必須賦值 n 個變量, 此時最後一個變量是 undefined
  • 對於展開運算符, 它必定展開 n - 1 個值, 執行 n 次 next(), 因此它必定會執行完生成器函數
  • 其中 yield* 和展開運算符都是必定會消費完可迭代對象的, 因此它們不會調用 return() 方法, 而 for...of 和數組解構則有可能不會消費完可迭代對象, 此時它們都會調用 return()

參考資料

相關文章
相關標籤/搜索