본문으로 건너뛰기

generator(제네레이터)_ES6 노트 2

무료2016-04-03#JS#js生成器#生成器用法#JavaScript生成器

FireFox 중의 해당 특성의 실현자의 친술에 근거하여, 몇 가지 상세와 응용 시나리오를 보충합니다

서론

실제로, 이전에 [黯羽轻揚: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)解析 를 참조하십시오

三.특징

제네레이터의 특징은 다음과 같습니다:

  • 普通の함수는自暂停할 수 없지만, 제네레이터함수는 할 수 있습니다

  • yieldfunction* 의 직접作用域中에서만 유효하며, 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 는 호출자에게 한 잔의 물을요구하며, 두 번째 .next() 는 물을 제네레이터에渡し, 그 후 두 번째 yield 는 물을마십니다

이것은 双方向의交互過程으로, 실제 응용에서는, next() 의 반환값에 근거하여 제네레이터가 무엇을 필요로 하는지를 판단한 후, 그 후次の .next() 로 그것을渡し, 복잡한 로직을 제네레이터중에隔離하며, 호출하는 것은 대화처럼轻松합니다

###2. 이터레이터를 종료하다

주의, 이터레이터 입니다. 이터레이터를 종료하는 방법은 2 종류 있습니다:

  • 이터레이터의 throw(err) 메서드로, 효과는 제네레이터중의 yield 式가 함수를 호출하여 에러를던지는 것과 같습니다

  • 이터레이터의 return(returnVal) 메서드로, 옵션 파라미터를 받아들이며, 파라미터는 value 로서 반환됩니다 (donetrue). 제네레이터는 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 中文站이 제공하는 무료 전자서적

댓글

아직 댓글이 없습니다

댓글 작성