はじめに
実際、之前に [黯羽轻揚:JavaScript ジェネレーター](/articles/javascript 生成器/) ですでにまとめましたが、ここは故意に重複しているわけではありません。之前は MDN ドキュメントを参考にしてまとめたもので、文法規則に重点を置いていました。本文は FireFox 中で該特性の実装者の親述に基づき、いくつかの詳細と応用シナリオを補充します
重複を避けるため、本文では文法規則(function* + yield)を説明しません。文法詳細は之前的な文章を参照してください
一.作用及び内部原理
generator(ジェネレーター)はイテレーターを作成するために使用され、文法は非常に簡潔です(function* + yield)
ジェネレーターは yield 文を実行する時、ジェネレーターのスタック構造(ローカル変数、パラメータ、一時値、ジェネレーター内部の現在の執行位置)がスタックから移出されます。しかし、ジェネレーターオブジェクトはこのスタック構造への参照(バックアップ)を保持しているため、後で .next() を呼び出すとスタック構造を再活性化して継続して執行できます
例えば:
// 定義生成器
var gen = function*() {
console.log('before yield 1');
yield 1;
console.log('before yield 2');
yield 2;
}
// 调用生成器返回迭代器
var iter = gen();
iter.next(); // before yield 1
// Object {value: 1, done: false}
iter.next(); // before yield 2
// Object {value: 2, done: false}
iter.next(); // Object {value: undefined, done: true}
iter.next(); // Object {value: undefined, done: true}
yield 文は関数本体を幾つかのセグメントに分割し、.next() は一度に 1 セグメントを実行します
二.イテレーターとジェネレーター
function* で定義されたものはイテレーターのジェネレーター(略してジェネレーター)と呼ばれます。それを呼び出すとイテレーターを返すからです
すべてのジェネレーターは内蔵の .next() と [Symbol.iterator]() メソッドの実装を持っており、私たちはループ部分の行為のみを编写する必要があります。function* 後の関数本体はループ構造のループ本体のようです。例えば:
function* gen(arr) {
for (var i = 0; i < arr.length; i++) {
yield arr[i];
}
}
var iter = gen([1, 2, 4]);
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.next()); // Object {value: 2, done: false}
その中で gen の作用は連続的な配列を「息をする」配列に変えることで、for ループは本来停まれないですが、yield は確かにそれを停まらせました。这也是ジェネレーター的一大特色です。この特点を利用して很多有趣なものを實現できます。例えばアニメーションで快速排序の過程を展示するなど、偽 コードは以下の通り:
function quicksort(arr) {
// sort
forloop {
updateSortedArr(); // 完成一趟排序
displaySortedArr(); // 展示本趟排序結果
}
return sortedArr;
}
もちろん、このようにするとアニメーションを見ることができません。n 趟の排序は瞬間に完成してしまうからです( 速すぎて、何も看不清啊 アニメーションなどない、根本没変好吗)。変通する方法を很容易に考えられます:まず每一趟の結果を存めておき、最後に展示します:
function quicksort(arr) {
var tmpArr = [];
forloop {
updateSortedArr(); // 完成一趟排序
tmpArr.push(sortedArr); // 存めておく
}
// アニメーションで排序過程を展示
anim(tmpArr);
}
每一趟の結果を取得した後、どのように展示したいかは自由にできます。感觉もあまり费力ではありません。では、もしアニメーションで快速排序の每一趟 2 つのポインターの移動を展示したい場合はどうでしょうか?私たちはまたポインター移動の tmpArr を記録する必要があるようです。もしこの配列が很大なら、もし。。。このようにすれば必ずより多くのメモリ空間を必要として已经发生过ことを記録します
仔细に考えてみると、排序過程中、アニメーション展示に必要なデータはすでにあります。では、排序しながらアニメーション展示することはできるでしょうか?もちろんできます:
function* quicksort(arr) {
forloop {
yield sortedArr;
// または
// yield step;
}
}
var iter = quicksort(arr);
// アニメーションで排序過程を展示
function anim() {
display(iter.next());
setTimeout(anim, 300);
}
anim();
そうです、ジェネレーターはループに「息をさせる」ことができ、息をする過程中にアニメーションを製造します
P.S. 実際にはジェネレーターがなくても、私たちはループを「停まらせる」ことができます。[黯羽轻揚:JavaScript 実装 yield](/articles/javascript 実装 yield/) を参照してください
P.S. 快速排序の具体的な詳細については、黯羽轻揚:排序算法之快速排序(Quicksort)解析 を参照してください
三.特点
ジェネレーターの特徴は以下の通り:
-
普通の関数は自暂停できませんが、ジェネレーター関数はできます
-
yieldはfunction*の直接作用域中でのみ有効で、function*中の匿名関数中のyieldは非法です -
無限序列を処理できます。無限大の配列を構築することはできませんが、ジェネレーターを使用して無限序列の構築規則を實現でき、無限序列を処理できます
-
配列を返す別の思路を提供します。配列ではなくジェネレーターを返し、時間で空間を換えます
-
複雑なループをリファクタリングでき、それを 2 つの部分に拆分し、データ生成部分をジェネレーターに変換し、その後 for...of でこれらのデータを遍历します
-
快速にイテレーターを製造でき、任意のオブジェクトを可迭代にできます。詳細は [黯羽轻揚:for…of ループ_ES6 ノート 1](/articles/for-of ループ-es6 ノート 1/) を参照してください
-
イテレーターを拡張しやすく、フィルタリングなどの操作を很容易に實現できます
第 2 点は注意が必要です。大多数のジェネレーターを紹介する資料はこの点に言及しませんが、私たちは確かにジェネレーター中で setTimeout で yield を遅延させることができません。ジェネレーターでイテレーターを拡張するのは不错的选择で、コードはとても自然です。示例は以下の通り:
// 拡張イテレーター
function* filter(isValid, iterable) {
for (var val of iterable) {
if (isValid(val)) {
yield val;
}
}
}
// test
function isValid(val) {
return val > 1;
}
for (var val of filter(isValid, [0, 1, 2, 4])) {
console.log(val);
}
ジェネレーターでイテレーターを包装すると、无缝衔接の美感がありますよね?
四.高級技巧
###1. 外部からジェネレーターのロジック流に影響を与える
イテレーターの next(returnVal) メソッドはオプションパラメータを受け取り、パラメータはジェネレーター中の上一条 yield 文の戻り値となります。このように呼び出し者は外部からジェネレーターのロジック流に影響を与えることができます。例えば:
function* gen() {
var water = yield 'give me a cup of pure water';
yield water.drink();
}
// test
var iter = gen();
console.log(iter.next());
console.log(iter.next({
name: 'pure water',
drink: function() {
return 'hmm, well';
}
}));
最初の yield は呼び出し者に一杯の水を要求し、2 番目の .next() は水をジェネレーターに渡し、その後 2 番目の yield は水を飲みます
これは双方向の交互過程で、実際の応用では、next() の戻り値に基づいてジェネレーターが何を必要とするかを判断し、その後次の .next() でそれを渡し、複雑なロジックをジェネレーター中に隔離し、呼び出すのは会話のように轻松です
###2. イテレーターを終了する
注意、イテレーター です。イテレーターを終了する方法は 2 種類あります:
-
イテレーターの
throw(err)メソッドで、効果はジェネレーター中の yield 式が関数を呼び出してエラーを投げるのと同じです -
イテレーターの
return(returnVal)メソッドで、オプションパラメータを受け取り、パラメータは value として返されます(doneはtrue)。ジェネレーターはfinallyコードブロックのみを実行し、不再恢復執行します
throw 示例は以下の通り:
// throw
function* gen() {
try {
yield 1;
yield 2;
} catch (err) {
console.log('error occurs: ' + err);
} finally {
console.log('clean up');
}
}
var iter = gen();
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.throw(new Error('err'))); // error occurs: Error: err
// clean up
// Object { value: undefined, done: true }
console.log(iter.next()); // Object {value: undefined, done: true}
throw(err) はイテレーターが異常に閉じることを示し、ジェネレーター内部に finally 中の清理工作を実行することを通知するために使用されます
一方、return() はイテレーターが正常に閉じることを示します。示例は以下の通り:
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.return('ok')); // clean up
// Object { value: "ok", done: true }
console.log(iter.next()); // Object {value: undefined, done: true}
注意:'ok' は value として立即に返され、次の .next() の時に返されるのではありません
P.S.Chrome49 はまだ return() をサポートしていません。FF では throw() 後も return() できますが、もし先に return() してその後 throw() するとエラーが報告されます
###3. イテレーターを拼接する
yield* iter はイテレーターを拼接でき、1 つのジェネレーター中で別のジェネレーターを呼び出すことをサポートします。例えば:
var gen1 = function* (){
yield 1;
yield 2;
}
var gen2 = function* (){
yield* gen1();
yield 3;
yield 4;
}
for (var val of gen2()) {
console.log(val); // 1 2 3 4
}
五.まとめ
ジェネレーターは執行流に「息をさせる」ことができ、停まれないものを暂停でき、ループをリファクタリングするために使用でき、無限序列を驾驭でき、イテレーターを包装できます。。。メリットは多いです
参考資料
- 《ES6 in Depth》:InfoQ 中文站が提供する無料電子書籍
コメントはまだありません