生成器(上)

55 阅读5分钟

生成器函数是一种具有暂停运行能力的执行模式,在js函数的基础上,生成器函数可以在函数执行过程中暂停自己,但是生成器并不只能暂停自己函数的执行,生成器每次暂停都可以从外界得到一个值,同时在暂停时向外界抛出一个值,和函数外可以建立起一种双向消息传递系统

迭代器和iterable

假如我们需要产生一系列值,每个值都和前面一个有特定的关系,看下面一个例子

const getSeriseValue = () => {
    let nextVal;
    
    return () => {
        if (nextVal === undefined) {
            nextVal = 1;
            return nextVal;
        } 
        nextVal = nextVal * 3 + 6;
        return nextVal;
    }
};

const next = getSetiseValue();

next();//1
next();//9
next();//33

从本质上,可以说我们需要从一个地方持续不断的得到值,直到结束,如果使用迭代器的形式,代码会变成如下

const getSeriesValue = () => {
    let nextVal;
    return {
        [Symbol.iterator]: function() { return this},  
        next: () => {
            if (nextVal === undefined) {
               nextVal = 1;
                return {value: nextVal, done: false };
            }
            nextVal = nextVal * 3 + 4;
            return {value:nextVal, done:false };
        }
    }
};

当调用getSeriesValue函数时得到一个对象,这个对象包含一个next函数,调用next函数得到一个对象,这个对象有两个属性, done 是一个boolean值,表示迭代器的状态,为true表示迭代结束,迭代器不会产生值了,value携带产生的值,

js遵守了迭代器的规则,并且在es6的时候推出了for...of循环,这个for...of循环可以不断的执行, 直到得到的结果对象的done是true,for...of循环会调用你的对象内部的Symbol.iterator属性方法,要求返回一个对象,这个对象必须要有next函数,这个next函数必须返回的对象的形式为{value:...,done:....}, (Symbol.iterable是Es6内建的一个标识符, 这个标识符是独一无二的)

const seriesValue = getSeriesValue();
for (let value of seriesValue) {
    if (value > 500) {
        break;
    }
    console.log(value);
    //1
    //7
    //25
    //79
    //241
}

可能最常见的用法是从数组得到一系列值,比如下面

const a = [1, 3, 5, 7, 9];

for (const v of a) {
    console.log(v);
}
//1 3 5 7 9

上面能执行成功完全是因为es6让数组对象都拥有了Symbol.iterable属性,调用这个属性函数会得到一个迭代器,自己写的字面量对象,如果你不特意去实现Symbol.iterable方法并且返回一个迭代器对象,是使用不了for...of的

iterable

上面的例子里,迭代器对象都包含一个next, 而当对象有Symbol.iterable属性,并且调用这个函数返回了一个迭代器,就称它为可迭代对象(iterable),上例中的seriesValue就是一个迭代器,同时它有Symbol.iterable属性,这个属性函数调用尽管是返回this,但是因为本身自己就是一个迭代器,也符合调用迭代器的Symbol.iterable得到一个迭代器的规则,所以它同时还是一个可迭代对象,Symbol.iterable函数返回this变成可迭代对象的用法还挺常见的, ,其实for..of就是期待一个可迭代对象,而不是迭代器.

总结

通过上面例子可以得到迭代器和可迭代对象的特性,我们描述这个特性,迭代器要求有一个next函数,而且这个函数返回{value:....,done:....},可迭代对象要求可以通过Symbol.iterable属性函数调用得到一个迭代器,我们把这两种特性描述为迭代器协议和生成器协议

生成器的双向消息传递

function *foo (x) {
    const y = x * (yield "Hello");
    return y;
}

//执行生成器函数,得到一个迭代器
const it = foo(6);//

//执行迭代对象
let ans = it.next(2);//传入2,没有用,会被忽略 
//ans为迭代结果,值为对象{value: "hello world", done: false}

console.log(ans.value); // "Hello"

ans = it.next(7);//替代(yield "Hello") ,变成 x * 7;

console.log(ans.value);//42




可以使用生成器函数生成一个迭代器(注意这里并且没有实现Symbol.Iterable接口,所以它不是可迭代对象,单纯是一个迭代器), 执行next函数,生成器函数就开始运行, 生成器函数内执行到 yield value暂停生成器函数的执行,并且将value抛给外部, 表现为迭代器执行next函数的返回值,这个返回值为一个迭代结果对象,结果如{value: ".....", done: ....}, 当继续next调用的时候传入一个参数,这个值会替代原来yield value 所在的位置继续执行, 如此循环往复,可以看到,

我们第一次调用next函数的时候传入参数是没有用的,因为第一次是用于整个生成器函数开始执行,并不是由yield 暂停,同时最后一次调用next函数的时候,也没有yield抛出一个值了,这个时候return 的值就作为最后一次调用得到的迭代结果对象的value属性的值

生成器迭代器

可迭代对象可以产出迭代器,这个迭代器实现了可迭代协议,同时也是一个可迭代对象,我们还可以覆写上面的例子

function *getSeriesValue() {
    let nextVal;
    while(true){
        if (nextVal === undefined) {
            nextVal = 1;
        } else {
            nextVal = nextVal * 3 + 6;
        }

        yield nextVal;
    }
}

for (let v of getSeriesValue()) {
    if (v > 500) {
        break;
    }
    console.log(v);
}

for...of调用break后,会向迭代器发送一个信号终止,也可以使用迭代器的return函数,调用return函数后,所有的所需yield都会被忽略,传递给return函数的参数会作为迭代结果对象的value属性的值,

function *getSeriesValue() {
    let nextVal;
    while (true) {
    if (nextVal === undefined) {
        nextVal = 1;
    } else {
        nextVal = nextVal * 3 + 6;
    }
    yield nextVal;
  }
}

const it = getSeriesValue()
for (let v of it) {
    if (v > 500) {
        console.log(it.return("down").value); //down
    }
    console.log(v);
}


//1
//9
//33
//105
//321
//down
//969

使用return终止当前的迭代器后,done为true,所以for...of也就不会继续循环下去了。

总结

基于上述,我们可以认为生成器的一个用法就是用于生产一系列值,它生成一个迭代器同时也是可迭代对象,可以给for...of连续产出一系列值,或者自己next调用非连续的产出值