Generator Function(生成器函数)是 ES6 引入的新特性,该特性早就出现在了 Python、C#等其他语言中。
A generator is a function that can stop midwayand then continue from where it stopped. Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。生成器与迭代器有着错综复杂的联系,在学习生成器前我们需要先了解下迭代器iterators。

可迭代协议迭代器协议。可迭代协议允许 JavaScript 对象去定义或定制它们的迭代行为, 例如(定义)在一个for..of结构中什么值可以被循环(得到)。一些内置类型都是内置的可迭代类型并且有默认的迭代行为, 比如 ArrayorMap, 另一些类型则不是 (比如Object) 。为了变成可迭代对象, 一个对象必须实现 @@iterator方法, 意思是这个对象(或者它原型链 prototype chain 上的某个对象)必须有一个名字是Symbol.iterator的属性。可迭代对象:在 ES6 中常用的集合对象(数组、Set/Map集合)和字符串都是可迭代对象,这些对象都有默认的迭代器和Symbol.iterator属性。String,Array,TypedArray,MapandSet是所有内置可迭代对象, 因为它们的原型对象都有一个 @@iterator 方法。我们可以用Symbol.iterator来访问对象的默认迭代器。



// String 是一个内置的可迭代对象:
var someString = "hi";
typeof someString[Symbol.iterator];          // "function"

// String 的默认迭代器会一个接一个返回该字符串的字符:
var iterator = someString[Symbol.iterator]();
iterator + "";                               // "[object String Iterator]"
 
iterator.next();                             // { value: "h", done: false }
iterator.next();                             // { value: "i", done: false }
iterator.next();                             // { value: undefined, done: true }

// 一些内置的语法结构,比如 spread operator (展开语法:[...val]),内部也使用了同样的迭代协议:
[...someString]                              // ["h", "i"]

// 例如对于一个数组Symbol.iterator获得了数组这个可迭代对象的默认迭代器,并操作它遍历了数组中的元素。
var list = [11, 22, 33]
var iterator2 = list[Symbol.iterator]()
console.log(iterator2.next()) // { value: 11, done: false }





es6循环相加 es6 循环依赖_es6 依赖循环


es6循环相加 es6 循环依赖_es6循环相加_02


迭代器协议定义了一种标准的方式来产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值。当一个对象只有满足下述条件才会被认为是一个迭代器:它实现了一个next()的方法并且next 方法必须要返回一一个对象,该对象有两个必要的属性: done和value。

在 JavaScript 中,迭代器iterators是一个对象,它定义一个序列,并在终止时可能返回一个返回值。 更具体地说,迭代器是通过使用 next()方法实现迭代器协议Iterator protocol的任何一个对象,该方法返回具有两个属性的对象: value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true 。如果 valuedone 一起存在,则它是迭代器的返回值。Javascript中最常见的迭代器是Array迭代器,它只是按顺序返回关联数组中的每个值。ES6 新的数组方法、集合、for-of 循环、展开运算符(...)甚至异步编程都依赖于迭代器(Iterator )实现。

你可以创建一个简单的范围迭代器,例如例1:


// 定义了从开始(包括)到结束(独占)间隔步长的整数序列,最终返回值是它创建的序列的大小,由变量iterationCount跟踪。

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
    let nextIndex = start;
    let iterationCount = 0;

    const rangeIterator = {
       next: function() {
           let result;
           if (nextIndex < end) {
               result = { value: nextIndex, done: false }
               nextIndex += step;
               iterationCount++;
               return result;
           }
           return { value: iterationCount, done: true }
       }
    };
    return rangeIterator;
}

// 使用这个迭代器看起来像这样:
let it = makeRangeIterator(1, 10, 2);

let result = it.next();
while (!result.done) {
 console.log(result.value); // 1 3 5 7 9
 result = it.next();
}

console.log("Iterated over sequence of size: ", result.value); // Iterated over sequence of size:  5


生成器函数:虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。 生成器函数使用function*语法编写。 最初调用时,生成器函数不执行任何代码,而是返回一种称为Generator的迭代器。 通过调用生成器的下一个方法消耗值时,Generator函数将执行,直到遇到yield关键字。可以根据需要多次调用该函数,并且每次都返回一个新的Generator,但每个Generator只能迭代一次。

上面的例1可改写为:


function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    for (let i = start; i < end; i += step) {
        yield i;
    }
}
var a = makeRangeIterator(1,10,2)
a.next()


es6循环相加 es6 循环依赖_迭代_03



一个函数是一段完整的代码,调用一个函数就是传入参数,然后返回结果:


function foo(x) {
    return x + x;
}

var r = foo(1); // 调用foo函数


函数在执行过程中,如果没有遇到return语句(函数末尾如果没有return,就是隐含的return undefined;),控制权无法交回被调用的代码。

generator跟函数很像,定义如下:


function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}


generator和函数不同的是,generator由function*定义(注意多出的*号),并且,除了return语句,还可以用yield返回多次。

调用generator对象有两个方法,一是不断地调用generator对象的next()方法; 第二个方法是直接用for ... of循环迭代generator对象,这种方式不需要我们自己判断done

例如:


// 传统方法写一个产生斐波那契数列的函数
function fib(max) {
    var t, a = 0, b = 1, arr = [0, 1];
    while (arr.length < max) {
        [a, b] = [b, a + b];
        arr.push(b);
    }
    return arr;
}

// 测试:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 

// 函数只能返回一次,所以必须返回一个Array。但是,如果换成generator,就可以一次返回一个数,不断返回多次。用generator改写如下:
function* fib(max) {
    var t,a = 0, b = 1, n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}
// 直接调用试试:会发现xx()并不会执行xx函数,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是迭代器对象(Iterator Object)。
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

// 调用方法一之不断地调用generator对象的next()方法
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}

// 调用方法二之直接用for ... of循环迭代generator对象
for (var x of fib(10)) {
    console.log(x); // 依次输出0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}


好处:Generator能避免异步回调代码的回调地狱写法


// 想要按顺序调用abc三个请求(用jQuery的Ajax)
$.get('a.html',function(dataa) {
    console.log(dataa,1);
    $.get('b.html',function(datab) {
        console.log(datab,2);
        $.get('c.html',function(datac) {
            console.log(datac,3);
        });
    });
});

//  用Generator改写
function request(url) {
  $.get(url, function(response){
    it.next(response);
  });
}
function* ajaxs() {
    console.log(yield request('a.html'));
    console.log(yield request('b.html'));
    console.log(yield request('c.html'));
}
var it = ajaxs();
it.next();
// 看上去是同步的代码,实际执行是异步的。


es6循环相加 es6 循环依赖_es6循环相加_04


es6循环相加 es6 循环依赖_es6循环相加_05


上面例子若用promise改写:https://jsbin.com/baribiyoda/edit?html,output


// Promise的写法的缺点就是各种promise实例对象跟一连串的then,代码量大、行数多,满眼的promise、then、resolve看得头晕,而且每一个then都是一个独立的作用域,传递参数痛苦。
new Promise(function(resolve) {
    $.get('a.html',function(dataa) {
        console.log(dataa);
        resolve();
    });
}).then(function(resolve) {
    return new Promise(function(resolve) {
        $.get('b.html',function(datab) {
            console.log(datab);
            resolve();
        });
    });
}).then(function(resolve) {
    $.get('c.html',function(datac) {
        console.log(datac);
    });
});


es6循环相加 es6 循环依赖_es6 依赖循环_06



yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

next方法参数的作用,是为上一个yield语句赋值。由于yield永远返回undefined,这时候,如果有了next方法的参数,yield就被赋了值。带参数跟不带参数的区别是,带参数的情况,首先第一步就是将上一个yield语句重置为参数值,然后再照常执行剩下的语句。总之,区别就是先有一步先重置值,接下来其他全都一样。

注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。


function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
// 第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。
a.next() // Object{value:NaN, done:false}
// 第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN。
a.next() // Object{value:NaN, done:true}

// 如果向next方法提供参数,返回结果就完全不一样了:
var b = foo(5);
b.next() // { value:6, done:false }
// 第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;
b.next(12) // { value:8, done:false }
// 第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。
b.next(13) // { value:42, done:true }


es6循环相加 es6 循环依赖_迭代_07


本文引用参考如下:

迭代器和生成器developer.mozilla.org

es6循环相加 es6 循环依赖_es6循环相加_08

ES6 Generator 介绍 | AlloyTeamwww.alloyteam.com

es6循环相加 es6 循环依赖_ES6_09

ECMAScript 6入门es6.ruanyifeng.com Understanding Generators in ES6 JavaScript with Examplescodeburst.io 深度解析ES6 迭代器与可迭代对象的实现www.jianshu.com

es6循环相加 es6 循环依赖_迭代_10

【ES6】迭代器与可迭代对象segmentfault.com generatorwww.liaoxuefeng.com

es6循环相加 es6 循环依赖_迭代器_11

理解 ES6 Generator 函数www.jianshu.com

es6循环相加 es6 循环依赖_ES6_12