资讯专栏INFORMATION COLUMN

Generator函数的语法以及异步的应用

plokmju88 / 2406人阅读

摘要:返回的遍历器对象,可以依次遍历函数内部的每一个状态。由于函数就是遍历器生成函数,因此可以把赋值给对象的属性,从而使得该对象具有接口。函数从暂停状态恢复运行,它的上下文状态时不变的。从语义上讲,第一个方法用来启动遍历器对象,所以不用带有参数。

基本概念

Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。Generator函数有多种理解角度。语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
形式上,Generator函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是产出)。

yield表达式解释

1.语法。
[rv] = yield [expression];
expression:定义通过迭代器协议从生成器函数返回的值。如果省略,则返回undefined。
rv:返回传递给生成器的next()方法的可选值,以恢复其执行。
2.描述
yield关键字使生成器函数执行暂停,yield关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本return关键字。
yield关键字实际返回一个IteratorResult对象,它有两个属性,value和done。value属性是对yield表达式求值的结果,而done是false,表示生成器函数尚未完全完成。

*yield,导致生成器再次暂停并返回生成器的新值。下一次调用next()时,在yield之后紧接着的语句继续执行。
*throw用于从生成器中抛出异常。这让生成器完全停止执行,并在调用者中继续执行,正如通常情况下抛出异常一样。
*到达生成器函数结尾;在这种情况下,生成器的执行结束,并且IteratorResult给调用者返回undefined并且done为true。
*到达return语句。这种情况下,生成器的执行结束,并将IterratorResult返回给调用者,其值是由return语句指定的,并且done为true。

如果将可选值传递给生成器的next()方法,则该值将称为生成器当前yield操作返回的值。
在生成器的代码路径中yield运算符,以及通过将其传递给Generator.prototype.next()指定新的起始值的能力之间,生成器提供了强大的控制力。
3.实例.

function*countAppleSales(){
    var arr = [1,2,3];
    for (var i=0;i

Generator函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。另外需要注意,yield表达式只能用在Generator函数里面,用在其它地方都会报错。

function*f(){
    console.log("执行了!");
}
var generator = f();
generator.next();
//上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个Generator函数,就变成只有调用next方法时才会执行。

下面是另一个例子:

var arr=[1,[[2,3],4],[5,6]];
var flat = function*(a){
    a.forEach(function(item){
        if(typeof item!=="niumber"){
            yield*flat(item);
        }else{
            yield item;
        }
    });
}
for (var f of flat(arr)){
    console.log(f);
}

上面代码也会产生语句错误,因为forEach方法的参数是一个普通函数,但是在里面使用了yield表达式。一种修改方法是改用for循环。

var arr=[1,[[2,3],4],[5,6]];
var flat = function*(a){
    var length = a.length;
    for(var i=0;i

另外,yield表达式如果用在另一个表达式中,必须放在圆括号里面。

function*demo(){
    console.log("Hellow"+yield);//SyntaxError
    console.log("Hellow"+yield 123);//SyntaxError
    console.log("Hellow"+(yield));//ok
    console.log("Hellow"+(yield 123));//ok
}

yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

function*demo(){
    foo(yield "a",yield "b");//ok
    let input = yield;//ok
}
与Iterator接口的关系

任意一个对象的symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回一个遍历器对象。由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的symbol.iterator属性,从而使得该对象具有Iterator接口。

var myIterator = {};
myIterator[Symbol.iterator] = function*(){
    yield 1;
    yield 2;
    yield 3;
}
[...myIterator]//[1,2,3]

上面代码中,Generator函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了Iterator接口,可以被...运算遍历了。
Generator函数执行后,返回一个遍历器对对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

function* gen(){
  // some code
}
var g = gen();
g[Symbol.iterator]() === g
// true

上面代码中,gen是一个Generator函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。

next方法的参数

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

function*f(){
    for(var i=0;true;i++){
        var reset = yield i;
        if(reset){i=-1}
    }
}
var g = f();
g.next()//{value:0,done:false}
g.next()//{value:1,done:false}
g.next(true)//{value:0,done:false}

上面代码先定义了一个可以无限运动的Generator函数f,如果next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined。当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。
这个功能有很重要的语法意义。Generator函数从暂停状态恢复运行,它的上下文状态时不变的。通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数内部注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
再看一个例子。

function*foo(x){
    var y = 2*(yield(x+1));
    var z = yield(y/3);
    return (x+y+z);
}
// var a = foo(5);
// console.log(a.next());//{value:6,done:false};
// console.log(a.next());//{value:NaN,done:false}因为此时y是undefined
// console.log(a.next());//{value:NaN,done:true}


var b = foo(5);
console.log(b.next());//{value:6,done:false};
console.log(b.next(12));//{value:8,done:false} 此时y=2*第一个yield的返回值(12)。
console.log(b.next(13));//z=13,y=24,x=5.{ value:42, done:true }

上面代码中,第二次运行next方法的时候不带参数,导致y的值等于2*undefined(即NaN),除以3以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5+NaN+undefined,即NaN。
如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1=6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y/3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42.
注意:由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
再看一个通过next方法的参数,向Generator函数内部输入值的例子。

function*dataConsumer(){
    console.log("Started");
    console.log(`1. ${yield}`);
    console.log(`2. ${yield}`);
    return "result";
}
let genObj = dataConsumer();
function* dataConsumer(){
  console.log("Started");
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return "result";
}
alet genObj = dataConsumer();
genObj.next();
//Started
genObj.next("a");
//1.a
genObj.next("b");
//2.b

上面代码是一个很直观的例子,每次通过next方法向Generator函数输入值,然后打印出来。
如果想要第一次调用next方法时,就能够输入值,可以在Generator函数外面再包一层。

function wrapper(generatorFunction){
  return function(){
    let generatorObject = generatorFunction();
    generatorObject.next();
    return generatorObject;
  };
}
const wrapped = wrapper(function*(){
  console.log(`First input: ${yield}`);
  return "DONE";
});
wrapped().next("hello!");
// console.log(wrapped().next("hello!"))

上面代码中,Generator函数如果不用wrapper先包一层,是无法第一次调用next方法,就输入参数的。

for...of循环

for...of循环可以自动遍历Generator函数时生成Iterator对象,且此时不再需要调用next方法。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

上面代码使用for...of循环,依次显示5个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环中。
下面是一个利用Generator函数for...of循环,实现斐波那契数列的例子。

function*fibonacci(){
    let [prev,curr] = [0,1];
    for(;;){
        yield curr;
        [prev,curr] = [curr,prev+curr];
    }
}
for(let n of fibonacci){
    if(n>100)break;
    console.log(n);
}

利用for...of循环,可以写出遍历任意对象的方法。原生JavaScript对象没有遍历接口,无法使用for...of循环,通过Gneerator函数为它加上这个接口,就可以用了。

//Reflect.ownKeys()方法返回target对象自己的属性键的数组。
//Reflect.ownKeys({z:3,y:2,x:1});//["z","y","x"];
//Reflect.ownKeys([]);//["length"];
//Reflect.ownKeys([1,2,3,4]);//["1","2","3","4","length"]
function*objectEntries(obj){
  let propKeys = Reflect.ownKeys(obj);
  for(let propKey of propKeys){
    yield [propKey,obj[propKey]]
  }
}
let jane = {first:"jane",last:"Doe"};
for(let[key,value] of objectEntries(jane)){
  console.log(`${key}:${value}`);
}
// first: Jane
// last: Doe

上面代码中,对象jane原生不具备 Iterator 接口,无法用for...of遍历。这时,我们通过 Generator 函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator属性上面。

//Object.keys 返回一个所有元素为字符串的数组,其元素来自于从给定的object上面可直接枚举的属性。这些属性的顺序与手动遍历该对象属性时的一致。
// simple array
var arr = ["a", "b", "c"];
console.log(Object.keys(arr)); // console: ["0", "1", "2"]

// array like object
var obj = { 0: "a", 1: "b", 2: "c" };
console.log(Object.keys(obj)); // console: ["0", "1", "2"]

// array like object with random key ordering
var anObj = { 100: "a", 2: "b", 7: "c" };
console.log(Object.keys(anObj)); // console: ["2", "7", "100"]

// getFoo is a property which isn"t enumerable
var myObj = Object.create({}, {
  getFoo: {
    value: function () { return this.foo; }
  } 
});
myObj.foo = 1;
console.log(Object.keys(myObj)); // console: ["foo"]

/**/
function* objectEntries() {
  let propKeys = Object.keys(this);
  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: "Jane", last: "Doe" };
jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 扩展运算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解构赋值
let [x, y] = numbers();
x // 1
y // 2

// for...of 循环
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

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

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

相关文章

  • 《Node.js设计模式》基于ES2015+回调控制流

    摘要:以下展示它是如何工作的函数使用构造函数创建一个新的对象,并立即将其返回给调用者。在传递给构造函数的函数中,我们确保传递给,这是一个特殊的回调函数。 本系列文章为《Node.js Design Patterns Second Edition》的原文翻译和读书笔记,在GitHub连载更新,同步翻译版链接。 欢迎关注我的专栏,之后的博文将在专栏同步: Encounter的掘金专栏 知乎专栏...

    LiuRhoRamen 评论0 收藏0
  • ES6 - Generator函数

    摘要:最后,调用这个函数,得到一个遍历器对象并赋值给变量。值得注意的是如果函数内部没有部署代码块,那么遍历器对象的方法抛出的错误,将被外部代码块捕获。 本文已同步至我的个人主页。欢迎访问查看更多内容!如有错误或遗漏,欢迎随时指正探讨!谢谢大家的关注与支持! 一、什么是Generator函数 Generator函数是ES6标准中提出的一种异步编程的解决方案。这种函数与普通函数最大的区别在于它可...

    Ali_ 评论0 收藏0
  • 深入理解Generator

    摘要:如果你已经理解基础可以直接跳过这部分和语法部分,直接看深入理解的部分。的参数可以传入一个参数,来作为上一次的表达式的返回值,就像我们上面说的会让等于。 这篇文章旨在帮你真正了解Generator,文章较长,不过如果能花时间耐心看完,相信你已经能够完全理解generator 为什么要用generator 在前端开发过程中我们经常需要先请求后端的数据,再用拿来的数据进行使用网页页面渲染等操...

    Euphoria 评论0 收藏0
  • ES6-7

    摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...

    mudiyouyou 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0

发表评论

0条评论

plokmju88

|高级讲师

TA的文章

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