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
结构中什么值可以被循环(得到)。一些内置类型都是内置的可迭代类型并且有默认的迭代行为, 比如 Array
orMap
, 另一些类型则不是 (比如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 }
迭代器协议定义了一种标准的方式来产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值。当一个对象只有满足下述条件才会被认为是一个迭代器:它实现了一个next()
的方法并且next 方法必须要返回一一个对象,该对象有两个必要的属性: done和value。
在 JavaScript 中,迭代器iterators是一个对象,它定义一个序列,并在终止时可能返回一个返回值。 更具体地说,迭代器是通过使用 next()
方法实现迭代器协议Iterator protocol的任何一个对象,该方法返回具有两个属性的对象: value
,这是序列中的 next 值;和 done
,如果已经迭代到序列中的最后一个值,则它为 true
。如果 value
和 done
一起存在,则它是迭代器的返回值。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()
一个函数是一段完整的代码,调用一个函数就是传入参数,然后返回结果:
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();
// 看上去是同步的代码,实际执行是异步的。
上面例子若用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);
});
});
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个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 }
本文引用参考如下:
迭代器和生成器developer.mozilla.org
ES6 Generator 介绍 | AlloyTeamwww.alloyteam.com
ECMAScript 6入门es6.ruanyifeng.com Understanding Generators in ES6 JavaScript with Examplescodeburst.io 深度解析ES6 迭代器与可迭代对象的实现www.jianshu.com
【ES6】迭代器与可迭代对象segmentfault.com generatorwww.liaoxuefeng.com
理解 ES6 Generator 函数www.jianshu.com