资讯专栏INFORMATION COLUMN

ECMAScript6(13):Generator 函数

wangxinarhat / 1847人阅读

摘要:函数可以没有返回值,此时它依然返回一个并且在调用方法时一次行执行完函数内全部代码,返回。将一个可遍历结构解构,并逐一返回其中的数据。

Generator

Generator 函数是 es6 中的新的异步编程解决方案,本节仅讨论 Generator 函数本身,异步编程放在后面的部分。
Generator 函数之前也提到过,描述内部封装的多个状态,类似一个状态机,当然也是很好的 iterator 生成器。Generator 函数的基本形式如下:

function* gen(){
  yield status1;
  yield status2;
  //...
}

不难看出,Generator 函数在 function 关键字和函数名之间加了一个星号"*", 内部用 yield 返回每一个状态。

当然还有其他格式的定义:

//函数表达式
var gen = function*(){
  yield status1;
  //...
};

//对象方法
var obj = {
  *gen(){
    yield status1;
    //...
  }
};

Generator 函数调用时,写法和普通函数一样。但函数并不执行执行时,返回内部自带 iterator,之后调用该 iterator 的 next() 方法, 函数会开始执行,函数每次执行遇到 yield 关键字返回对应状态,并跳出函数,当下一次再次调用 next() 的时候,函数会继续从上一次 yield 跳出的下一跳语句继续执行。当然 Generator 函数也可以用 return 返回状态,不过此时,函数就真的运行结束了,该遍历器就不再工作了;如果函数内部所以的 yield 都执行完了,该遍历器一样不再工作了:

function* gen(){
  yield "hello";
  yield "world";
  return "ending";
}
var it = gen();
console.log(it.next());      //{value: "hello", done: false}
console.log(it.next());      //{value: "world", done: false}
console.log(it.next());      //{value: "ending", done: true}
console.log(it.next());      //{value: undefined, done: true}

注意:

return 返回的值,对应的 done 属性是 true。说明 return语句结束了遍历,iterator 不再继续遍历,即便后面还有代码和 yield。

Generator 函数可以没有 yield 返回值,此时它依然返回一个 iterator, 并且在 iterator 调用 next 方法时一次行执行完函数内全部代码,返回{value: undefined, done: true}。 如果有 return 语句,该返回值对应的 value 属性值为 return 表达式的值。

普通函数使用 yield 语句会报错

yield 可以用作函数参数,表达式参数:

function* gen(){
  console.log("hello" + (yield));    //yield 用作表达式参数必须加()
  let input = yield;
  foo(yield "a", yield "b");
}

Generator 函数的默认遍历器[Symbol.iterator]是函数自己:

function* gen(){}
var g = gen()
g[Symbol.iterator]() === g;    //true
next() 参数

yield 语句本身具有返回值,返回值是下一次调用 next 方法是传入的值。next 方法接受一个参数,默认 undefined:

function* f(){
  for(let i = 0; true; i++){
    var reset = yield i;
    if(reset) i = -1;
  }
}
var g = f();
console.log(g.next().value)          //0
console.log(g.next().value)          //1
console.log(g.next().value)          //2
console.log(g.next(true).value)      //0

上面 代码第3行var reset = yield i等号右侧是利用 yield 返回i, 由于赋值运算时右结合的,返回 i 以后,函数暂停执行,赋值工作没有完成。之后再次调用 next 方法时,将这次传入参数作为刚才这个 yield 的返回值赋给了 reset, 因此计数器被重置。

function* foo(x){
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}
var g = foo(5);
console.log(g.next());          //{value: 6, done: false}
console.log(g.next(12));        //{value: 8, done: false}
console.log(g.next(13));        //{value: 42, done: true}

第一次调用 next 函数不需要参数,作为 Generator 启动,如果带了参数也会被忽略。当然,如果一定想在第一次调用 next 时候就赋值,可以将 Generator 函数封装一下:

//一种不完善的思路,通常不强求这样做
function wrapper(gen){
  return function(){
    let genObj = gen(...arguments);
    genObj.next();       //提前先启动一次,但如果此时带有返回值,该值就丢了!
    return genObj;
  }
}
var gen = wrapper(function*(){
  console.log(`first input: "${yield}"`);
});
var it = gen();
it.next("Bye-Bye");       //first input: "Bye-Bye"
for...of

我们注意到,之前在 iterator 中,迭代器最后返回{value: undefined, done: true},其中值为 undefined 和 done 为 true 是同时出现的,而遍历结果不包含 done 为 true 时对应的 value 值,所以 Generator 的 for...of 循环最好不要用 return 返回值,因为该值将不会被遍历:

function* gen(){
  for(var i = 0; i < 5; i++){
    yield i;
  }
  return 5;
}
for(let v of gen()){
  console.log(v);       //依次输出 0, 1, 2, 3, 4, 没有 5
}

除了 for...of, Generator 还有很多简单用法。下面利用 fibonacci 数列,演示几种不同的 Generator 用法:

展开运算符

function* fib(n = Infinity){
  var a = 1, b = 1;
  while(n){
    yield a;
    [a, b] = [b, a + b];
    n--;
  }
}
console.log([...fib(10)]); //1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

解构赋值

function* fib(n = Infinity){
  var a = 1, b = 1;
  while(n){
    yield a;
    [a, b] = [b, a + b];
    n--;
  }
}
var [a, b, c, d, e, f] = fib();  //a=1, b=1, c=2, d=3, e=5, f=8

构造函数参数

function* fib(n = Infinity){
  var a = 1, b = 1;
  while(n){
    yield a;
    [a, b] = [b, a + b];
    n--;
  }
}
var set = new Set(fib(n));
console.log(set);  //Set(9) [1, 2, 3, 5, 8, 13, 21, 34, 55]

Array.from方法

function* fib(n = Infinity){
  var a = 1, b = 1;
  var n = 10;
  while(n){
    yield a;
    [a, b] = [b, a + b];
    n--;
  }
}
var arr = Array.from(fib(10));
console.log(arr);    //[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

遍历对象

function* entries(obj){
  for(let key of Object.keys(obj)){
    yield [key, obj[key]];
  }
}
var obj = {
  red: "#ff0000",
  green: "#00ff00",
  blue: "#0000ff"
};
for(let [key, value] of entries(obj)){
  console.log(`${key}: ${value}`);        //依次输出 "red: #ff0000", "green: #00ff00", "blue: #0000ff"
}
throw() 方法和 return() 方法

Generator 返回的遍历器对象具throw() 方法, 一般的遍历器用不到这个方法。该方法接受一个参数作为抛出的错误,该错误可以在 Generator 内部捕获:

function* gen(){
  while(1){
    try{
      yield "OK";
    } catch(e) {
      if(e === "a") console.log(`内部捕获: ${e}`);    //内部捕获: a
      else throw e;
    }
  }
}
var it = gen();
it.next();              //如果没有这一行启动生成器,结果仅输出:外部捕获: a
try{
  it.throw("a");
  it.throw("b");
  it.next();            //上一行错误为外部捕获,try 中的代码不在继续执行,故这一行不执行
} catch(e) {
  console.log(`外部捕获: ${e}`)    //外部捕获: b
}

throw参数在传递过程中和 next 参数类似,需要先调用一次 next 方法启动生成器,之后抛出的错误会在前一个 yield 的位置被捕获:

function* gen(){
  yield "OK";             //错误被抛到这里,不在内部 try 语句内无法捕获
  while(1){
    try{
      yield "OK";
    } catch(e) {
      console.log(`内部捕获: ${e}`);
    }
  }
}
var it = gen();
it.next();
try{
  it.throw("a");
} catch(e) {
  console.log(`外部捕获: ${e}`)    //外部捕获: a
}

注意: 不要混用 throw() 方法和 throw 语句,后者无法将错误抛到生成器内部。其次,throw 会终止遍历器,不能继续工作,而 throw 不会终止遍历器:

function* gen(){
  yield console.log("hello");
  yield console.log("world");
}

//throw 语句
var it1 = gen();
it1.next();           //hello
try{
  throw new Error();
} catch(e) {
  it1.next()         //world
}

//throw() 方法
var it2 = gen();
it2.next();           //hello
try{
  it2.throw();
} catch(e) {
  it2.next()         //遍历器被关闭无法执行, 静默失败
}

如果在遍历器内部抛出错误,遍历器中止,继续调用 next() 方法将得到{value: undefined, done: true}:

function* gen(){
  var x = yield "ok";
  var y = yield x.toUpperCase();
  var z = yield (x + y + z);
}

//throw 语句
var it = gen();
it.next();           //"ok"
try{
  it.next();
} catch(e) {
  console.log("Error Caught");   //Error Caught
} finally {
  it.next();         //{value: undefined, done: true}
}

return() 方法返回指定的值,并终止迭代器:

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

如果不给 return() 方法提供参数,默认是 undefined
如果 Generator 中有 try...finally 语句,return 会在 finally 执行完再执行:

function* numbers(){
  yield 1;
  try{
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
console.log(g.next().value);          //1
console.log(g.next().value);          //2
console.log(g.return("end").value);   //延迟到 finally 之后输出 -----
console.log(g.next().value);          //4                         |
console.log(g.next().value);          //5                         |
                                      //"end" <-------------------
console.log(g.next().value);          //undefined
yield* 语句

在一个 Generator 中调用另一个 Generator 函数默认是没有效果的:

function* gen(){
  yield 3;
  yield 2;
}
function* fun(){
  yield gen();
  yield 1;
}
var it = fun();
console.log(it.next().value);    //gen 函数返回的遍历器
console.log(it.next().value);    //1
console.log(it.next().value);    //undefined

显然第一次返回的结果不是我们想要的。需要使用 yield 解决这个问题。yield 将一个可遍历结构解构,并逐一返回其中的数据。

function* gen(){
  yield 3;
  yield 2;
}
function* fun(){
  yield* gen();
  yield 1;
}
var it = fun();
console.log(it.next().value);    //3
console.log(it.next().value);    //2
console.log(it.next().value);    //1
function* fun(){
  yield* [4,3,2];
  yield 1;
}
var it = fun();
console.log(it.next().value);    //4
console.log(it.next().value);    //3
console.log(it.next().value);    //2
console.log(it.next().value);    //1

被代理的 Generator 可以用return向代理它的 Generator 返回值:

function* gen(){
  yield "Bye";
  yield* "Hi"
  return 2;
}
function* fun(){
  if((yield* gen()) === 2) yield* "ok";
  else yield "ok";
}
var it = fun();
console.log(it.next().value);    //Bye
console.log(it.next().value);    //H
console.log(it.next().value);    //i
console.log(it.next().value);    //o
console.log(it.next().value);    //k
console.log(it.next().value);    //undefined

举例:

数组扁平化

//方法1:
var arr = [1,2,[2,[3,4],2],[3,4,[3,[6]]]];
function plat(arr){
  var temp = [];
  for(let v of arr){
    if(Array.isArray(v)){
      plat(v);
    } else {
      temp.push(v);
    }
  }
  return temp;
}
console.log(plat(arr));              //[1, 2, 2, 3, 4, 2, 3, 4, 3, 6]

//方法2:
function* plat2(arr){
  for(let v of arr){
    if(Array.isArray(v)){
      yield* plat2(v);
    } else {
      yield v;
    }
  }
}
var temp = [];
for(let x of plat2(arr)){
  temp.push(x);
}
console.log(temp);                    //[1, 2, 2, 3, 4, 2, 3, 4, 3, 6]

遍历二叉树

//节点
function Node(value, left, right){
  this.value = value;
  this.left = left;
  this.right = right;
}

//二叉树
function Tree(arr){
  if(arr.length === 1){
    return new Node(arr[0], null, null);
  } else {
    return new Node(arr[1], Tree(arr[0]), Tree(arr[2]));
  }
}
var tree = Tree([[[1], 4, [5]], 2, [[[0], 6, [9]], 8, [7]]]);

//前序遍历
function* preorder(tree){
  if(tree){
    yield tree.value;
    yield* preorder(tree.left);
    yield* preorder(tree.right);
  }
}
//中序遍历
function* inorder(tree){
  if(tree){
    yield* inorder(tree.left);
    yield tree.value;
    yield* inorder(tree.right);
  }
}
//后序遍历
function* postorder(tree){
  if(tree){
    yield* postorder(tree.left);
    yield* postorder(tree.right);
    yield tree.value;
  }
}

var _pre = [], _in = [], _post = [];
for(let v of preorder(tree)){
  _pre.push(v);
}
for(let v of inorder(tree)){
  _in.push(v);
}
for(let v of postorder(tree)){
  _post.push(v);
}
console.log(_pre);     //[2, 4, 1, 5, 8, 6, 0, 9, 7]
console.log(_in);      //[1, 4, 5, 2, 0, 6, 9, 8, 7]
console.log(_post);    //[1, 5, 4, 0, 9, 6, 7, 8, 2]

Generator 实现状态机:

//传统实现方法
var clock1 = function(){
  var ticking = false;
  return {
    next: function(){
      ticking = !ticking;
      if(ticking){
        return "Tick";
      }else{
        return "Tock";
      }
    }
  }
};
var ck1 = clock1();
console.log(ck1.next());      //Tick
console.log(ck1.next());      //Tock
console.log(ck1.next());      //Tick

//Generator 方法
var clock2 = function*(){
  while(1){
    yield "Tick";
    yield "Tock";
  }
};
var ck2 = clock2();
console.log(ck2.next().value);      //Tick
console.log(ck2.next().value);      //Tock
console.log(ck2.next().value);      //Tick
Generator 函数中的 this

在ES6中, 规定了所有 iterator 是 Generator 函数的实例:

function* gen(){}
var it = gen();
it instanceof gen;                                  //true
console.log(gen.__proto__);                         //GeneratorFunction
console.log(gen.__proto__.__proto__);               //Function
console.log(gen.constructor);                       //GeneratorFunction
console.log(gen.__proto__.constructor);             //GeneratorFunction
gen.prototype.sayHello = function(){
  console.log("hello");
}
it.sayHello();     //"hello"

但是 Generator 函数中的 this 并不指向生成的 iterator:

function* gen(){
  this.num = 11;
  console.log(this);
}
var it = gen();
console.log(it.num);     //undefined
it.next();               //Window

var obj = {
  * fun(){
    console.log(this);
  }
}
var o_it = obj.fun();
o_it.next();              //obj

由上面这个例子不难看出,Generator 函数中的 this 和普通函数是一样的。不过,可不可以把 Generator 函数作为构造函数呢?显然是不行的:

function* gen(){
  this.num = 11;
}
gen.prototype.say = function(){console.log("hello")}
var a = new gen();    //TypeError: gen is not a constructor
Generator 函数推导

ES7 在数组推导的基础上提出了 Generator 函数推导,可惜这个功能目前还不能使用:

let gen = function*(){
  for(let i = 0; i < 6; i++){
    yield i;
  }
};
let arr = [for(let n of gen()) n * n];
//相当于:
let arr = Array.from(gen()).map(n => n * n);
console.log(arr); [0,1,4,9,16,25]

Generator 数组推导,利用惰性求值优化系统资源利用:

var bigArr = new Array(10000);
for(let i = 0; i < 10000; i++){
  bigArr.push(i);
}
//....其他代码
//使用 bigArr 之前很久就分配了内存
console.log(bigArr[100]);

var gen = function*(){
  for(let i = 0; i < 10000; i++){
    yield i;
  }
};
//....其他代码
//使用 bigArr 时才分配内存
var bigArr = [for(let n of gen()) n];
console.log(bigArr[100]);
应用举例

优化回调函数

//伪代码
function* main(){
  var result = yield request("http://url.com");
  var res = JSON.parse(result);
  console.log(res.value);
}
function request(url){
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url);
  xhr.onreadystatechange = function(){
    if(xhr.readyState == 4 && xhr.status == 200){
      it.next(xhr.response);
    }
  }
  xhr.send();
}
var it = main();
it.next();

另一个例子:

//伪代码
//遇到多重回调函数,传统写法:
step1(function(value1){
  step2(value1, function(value2){
    step3(value2, function(value3){
      step4(value3, function(value4){
        //do something
      });
    });
  });
});
//利用 Generator 写:
function* gen(){
  try{
    var value1 = yield step1();
    var value2 = yield step2(value1);
    var value3 = yield step3(value2);
    var value4 = yield step4(value3);
  } catch(e) {
    //Handle the error form step1 to step4
  }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97444.html

相关文章

  • ECMAScript6(16):异步编程

    摘要:异步编程程序执行分为同步和异步,如果程序每执行一步都需要等待上一步完成才能开始,此所谓同步。因此异步编程十分重要。 异步编程 程序执行分为同步和异步,如果程序每执行一步都需要等待上一步完成才能开始,此所谓同步。如果程序在执行一段代码的同时可以去执行另一段代码,等到这段代码执行完毕再吧结果交给另一段代码,此所谓异步。比如我们需要请求一个网络资源,由于网速比较慢,同步编程就意味着用户必须等...

    曹金海 评论0 收藏0
  • ECMAScript6(14):iterator 迭代器

    摘要:由于中引入了许多数据结构算上原有的包括等等数组需要一个东西来管理他们这就是遍历器。数组默认遍历器遍历值相当于依次输出依次输出依次输出依次输出不难看出默认得到值而只能得到索引。即遍历器的本质就是一个指针。 由于 ES6 中引入了许多数据结构, 算上原有的包括Object, Array, TypedArray, DataView, buffer, Map, WeakMap, Set, We...

    toddmark 评论0 收藏0
  • ECMAScript6(0):ES6简明参考手册

    摘要:允许我们把水平的代码回调函数的地狱转换为竖直的代码在之前,我们使用或是,现在我们有了这里我们有个,执行成功时调用的函数和失败时调用的函数。使用的好处使用嵌套的回调函数处理错误会很混乱。 es6-参考手册 该手册包括ES2015[ES6]的知识点、技巧、建议和每天工作用的代码段例子。欢迎补充和建议。 var 和 let / const 除了var,我们现在有了两种新的标示符用来存储值——...

    ARGUS 评论0 收藏0
  • ECMAScript6(2):解构赋值

    摘要:解构赋值解构赋值简单来说就是对应位置数组或对应键名对象的变量匹配过程。字符串集合使用结构赋值实现叠加并交换变量对象的解构赋值对象的解构赋值与变量位置次序无关只取决于键名是否严格相等。 解构赋值 解构赋值简单来说就是 对应位置(数组)或对应键名(对象)的变量匹配过程。如果匹配失败, 对于一般变量匹配不到结果就是 undefined, 对于具有展开运算符(...)的变量结果就是空数组。 数...

    tinylcy 评论0 收藏0
  • ECMAScript6(10):Symbol基本类型

    摘要:基本类型是一种解决命名冲突的工具。这样,就有了个基本类型和个复杂类型使用需要注意以下几点和一样不具有构造函数,不能用调用。判断对象是否某个构造函数的实例,运算符会调用它是一个数组对象属性。即,当存在时,以此为构造函数构建对象。 Symbol基本类型 Symbol 是一种解决命名冲突的工具。试想我们以前定义一个对象方法的时候总是要检查是否已存在同名变量: if(String && Str...

    lavor 评论0 收藏0

发表评论

0条评论

wangxinarhat

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<