Javascript学习笔记-生成器

Javascript-生成器.png

在Javascript中,普通函数一旦开始运行在函数运行结束之前是不会中断的,而ES6引入的Generator(生成器)可以使得函数可以发生中断,分步运行。

1 Iterable和Iterator

在说明如何创建和使用Generator之前,先要说到ES6中迭代相关问题。

1.1 Iterable

在ES6中,可以使用for of的方式来循环遍历对象:

for(let i of [1,2,3,4]){
    console.log(i);
}

一个对象如果能够进行这样的迭代循环就被称为iterable(可迭代)。在iterable的对象中包含一个属性[Symbol.iterator],这个属性的值是一个function,而这个function是一个iterator(迭代器)

var obj = {
    [Symbol.iterator]:function iterator(){
        // TODO 省略掉了iterator的实现
    }
}

1.2 Iterator

一个iterator是返回指一个包含next属性,其值为是一个方法,且调用该next方法会返回对象符合{value:xx, done: xx}结构的值。

var iterator = function() {
    let i = 0;
    return {
        next() {
            i++;
            return {value:i, done: i > 3}
        }
    }
}

1.3 ES6中的迭代

自定义iterable对象的完整创建过程如下:

var o = {
    [Symbol.iterator]: function() {
        let i = 0;
        return {
            next() {
                i++;
                return {value:i, done: i > 3}
            }
        }
    }
}

在使用for of进行迭代操作的时候,每一次循环都会调用iteratornext函数,将每一次next调用返回的value作为迭代的值,同时会一直迭代直到done的值返回true为止

for(let i of o) {
    console.log(i); // 1, 2, 3 因为当i>3的时候done返回为true,所以就停止执行
}

同时ES6中引入了[...x](分离操作)的语法糖,可以将iterable对象迭代的结果值放到一个数组中:

var arr = [...o]; 
console.log(arr); // [1,2,3]

2. Generator的创建

生成器自身是iterable的,同时自身是一个iterator对象

2.1 使用function *()创建

生成器通常使用function *()的方式进行创建,声明中*的位置可以直接跟在function关键字后面也可以在函数名前面,创建的生成器函数中包含yield关键字。

function *g() { // 也可以是function* g()
  yield 10;
}

2.2 使用GeneratorFunction构造方法创建

也可以使用GeneratorFunction构造方法的形式创建生成器,不过用起来就比较复杂了,其中GeneratorFunction(...arguments, exec)接受的最后一个参数exec为生成器方法体,前面的参数为函数构造器的参数

var GeneratorFunction = Object.getPrototypeOf(function *(){}).constructor;
var g = new GeneratorFunction('a', 'yield a');
// 上面的构造过程相当于
function *g(a) {
  yield a;
}

通常使用function *()声明,因为看起来更适合阅读。

3 Generator的使用

3.1 获取iterator

在使用生成器的时候,直接调用构造方法,就可以返回一个iterator(迭代器)

function *g() {
  yield 1;
}
var it = g(); // 生成一个iterator

之前说过Generator其自身是iterable的同时是iterator,主要表现在:

function *g(){
  yield 1;
}
var it = g();
console.log(it[Symbol.iterator]() === it);

3.2 顺序化异步执行

构造了迭代器对象iterator后,和普通的函数不同,生成器函数并没有进行执行,只有在调用了next方法(也可以是return或者throw)方法以后,才会开始执行,且按照以下方式可以判断执行结果:

  1. 执行到第一个yiled停止,返回{value: x, done: x}格式的数据,其中将yield关键字后的值作为value的值输出,并将done设置为false,再次调用next方法,函数会再次向后执行
  2. 如果调用next方法以后函数向后执行不存在yield,则此时valueundefined(或者return的值),done将设置为true
  3. 如果调用next方法以后函数向后执行存在return关键字,则此时valuereturn的值,done将设置为true

// 生成器最后不返回值
function *g1() {
  var a = yield 3;
  console.log(a);
}
var it1 = g1();
console.log(it1.next()); // {value: 3, done: false};
console.log(it1.next()); // {value: undefined, done: true};
// 生成器有return语句
function *g2() {
  var a = yield 3;
  console.log(a);
  return 2;
}
var it2 = g2();
console.log(it2.next()); // {value: 3, done: false};
console.log(it2.next()); // {value: 2, done: true};

除了通过next方法可以获取到yield之后到值以外,next方法可以添加一个参数,参数将把yield表达式替换为该参数值

function *g1() {
  var a = yield 3;
  console.log('a=' + a);
}
var it1 = g1();
it1.next(); // 运行到`yield 3`停止,并返回{value: 3, done: false};
it1.next(1); // 继续运行,同时把`yield 3`替换为1,输出生成器函数中console.log('a=' + a)语句

要注意三点:

如果调用return,迭代器会立即结束,value值为return的值,done设置为true

function *g(){
  yield 1;
  yield 2;
} 
var it = g();
console.log(it.return(3)); // {value: 3, done: true};
console.log(it.next()); // {value: undefined, done: true} 

如果生成器方法中存在return语句,执行到return语句后会将返回值作为value的值,且done会变为true,继续调用next方法,不能获取到yield的值,value始终为undefineddone始终为true

function *g(){
  yield 1;
  return 2;
  yield 3;
  return 4;
}
var it = g();
console.log(it.next()); // {value: 1, done: false};
console.log(it.next()); // {value: 2, done: true};
console.log(it.next()); // {value: undefined, done: true};
console.log(it.next()); // {value: undefined, done: true};

如果调用迭代器的throw方法或者生成器函数中存在throw操作,将抛出异常,迭代器运行结束

function *g() {
  yield 1;
  throw 2;
}
var it = g();
console.log(it.next()); // {value: 1, done: false};
console.log(it.next()); // 提示异常,并终止运行
// 或者
function *g() {
  yield 1;
  yield 2;
}
var it = g();
console.log(it.throw(2)); // 提示异常,并终止运行
console.log(it.next()); // 不会执行
3.2.1 在异步方法中的使用

生成器因为可以进行中断运行,且利用next方法控制运行,所以对于异步方法,就可以使用顺序的方式来编写代码,而不用在回调中进行数据的处理。

// 普通方法执行
function f() {
  var a;
  setTimeout(_=> {
    a = 3;
  }, 1000);
  var b = a * 100; 
  console.log('b=' + b);
}
f(); // 输出b=NaN
// 使用生成器执行
function *g() {
  var a = yield setTimeout(_=> {
    it.next(3);
  }, 1000);
  var b = a * 100; // 
  console.log('b=' + b);
}
var it = g();
it.next(); // 1s后会输出b=300
3.2.2 和Promise的联合用法

和在异步方法中的控制过程一样,可以在Promise中使用来控制程序的执行

function *g() {
  var p = yield Promise.resolve(3).then(fulfill => {
    setTimeout(_=>{
      it.next(fulfill);
    }, 1000)
  });
  console.log('p=' + p);
}
var it = g();
it.next(); // 1s后输出 p=3

3.3 [Symbol.iterator]的迭代器

由于生成器的特性,于是用来创建iterable对象的iterator

var obj = {
  [Symbol.iterator]: function *g() {
    let i = 0;
    while(i < 3) {
      i++;
      yield i;
    }
    return 'finish' + i;
  }
}
for(let o of obj) {
  console.log(o); // 1, 2, 3
}

4 其他

4.1 形实转换程序

在Javascript中使用一个函数来来封装其他函数,并将需要的参数也一并封装

function f(x, y){
  return x + y;
}
function thunk() {
  return f(3, 4);
}
thunk(); // 返回7

对于存在回调的函数,可以采用增加一个回调函数作为参数

function f(x, y, cb){
  setTimeout(_=>{
    cb(x, y);
  },1000)
}
function thunk(cb) {
  return f(3, 4, cb);
}
thunk((x, y)=>{
  console.log(x+y); // 7;
});

利用这种写法,我们可以对很多异步方法进行重新封装,可以让我们的焦点放到回调函数中,可以一定程度将参数输入和输出的处理过程进行分开开发。

4.2 生成器委托

在生成器中,可以嵌套iterable对象,嵌套iterable对象的时候并使用yield*,当执行到yield* iterable的时候,会将运行流程交给iterable对象的iterator。并在继续执行时调用其next方法。

function *g1() {
  yield 2;
  yield 3;
}
function *g2() {
  yield 1;
  yield* g1(); // 或者yield *g1();
  yield 4;
}
var it = g2();
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};
console.log(it.next()); // {value: undefined, done: true};

5 总结

生成器是一个很有意思的东西,使用生成器可以将异步操作进一步升级,用一种同步编码风格去处理异步的情况。

6 参考

《你不知道的Javascript(中卷)》
MDN Generator

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容