最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 理解 Generator

    正文概述 掘金(sherryhe)   2021-06-26   585

    这是我参与更文挑战的第16天,活动详情查看:更文挑战


    我们都知道,JavaScript 中普通函数只返回一次,或者不返回。

    当我们有需要多次返回的需求时,一种特殊的函数 —— Generator 就应运而生了,它可以通过表达式 yield 来多次返回我们想要的值。再配合迭代器,就可以让我们轻松地创建数据流。

    下面让我们来认识 Generator 函数。

    Generator 函数

    为了和普通函数加以区分,Generator 函数规定了一种特殊语法,就是使用 function* ,像这样:

    function* generateSequence() {
      yield 1;
      yield 2;
      return 3;
    }
    

    Generator 函数的表现也和普通函数不同,它被调用的时候不会直接运行里面的代码,而是先返回一个特殊的 Generator 对象,再用这个对象来执行函数。

    像这样:

    function* generateSequence() {
      yield 1;
      yield 2;
      return 3;
    }
    
    // "generator function" creates "generator object"
    let generator = generateSequence();
    alert(generator); // [object Generator]
    

    到这里,函数里面的代码还没有被执行。

    理解 Generator

    想要执行它,就需要调用 Generator 的核心方法 —— next(),它会执行到最近一个 yield <value> 表达式(省略 value的话会返回 undefined)。然后函数就会暂停执行并返回 yield 后面的值。

    next()方法返回一个对象,包含以下两个属性:

    • value: yield出来的值
    • done: 函数全部执行完返回true,否则返回false

    例如,我们创建一个 generator然后获取它第一个yield的值:

    function* generateSequence() {
      yield 1;
      yield 2;
      return 3;
    }
    
    let generator = generateSequence();
    
    let one = generator.next();
    
    alert(JSON.stringify(one)); // {value: 1, done: false}
    

    现在函数只执行到了第二行,我们只拿到了第一个返回值。

    理解 Generator

    我们再次调用 generator.next(), 函数会继续执行并返回下一个 yield:

    let two = generator.next();
    
    alert(JSON.stringify(two)); // {value: 2, done: false}
    

    理解 Generator

    第三次调用,函数将会执行到 return结束语句:

    let three = generator.next();
    
    alert(JSON.stringify(three)); // {value: 3, done: true}
    

    理解 Generator

    现在 Generator 执行完,我们可以看到最终结果返回了 done: truevalue: 3

    这时再继续调用将不会有任何动作,会始终返回 {done: true}

    Generator 是可迭代的

    通过 next()方法也许你已经发现了,Generator 是可以迭代的。

    我们可以用 for..of 来遍历它:

    function* generateSequence() {
      yield 1;
      yield 2;
      return 3;
    }
    
    let generator = generateSequence();
    
    for(let value of generator) {
      alert(value); // 1, then 2
    }
    

    看起来是不是比调用 .next().value优雅一点?

    但是注意,这里只输出了12就结束了,没有输出3

    因为 done: true 的时候, for…of 循环就忽略了最后的返回值。所以如果希望用for...of来遍历所有的值,就都要用 yield来返回:

    function* generateSequence() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    let generator = generateSequence();
    
    for(let value of generator) {
      alert(value); // 1, then 2, then 3
    }
    

    既然 generator是可以迭代的,我们就可以使用相关特性了,例如扩展运算符 ...:

    function* generateSequence() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    let sequence = [0, ...generateSequence()];
    
    alert(sequence); // 0, 1, 2, 3
    

    这里,...generateSequence() 的作用是 Generator函数迭代的值依次放入数组。

    在迭代器中使用Generator

    在之前介绍的 迭代器 一节中,我们创建了一个范围迭代对象,返回 form…to

    我们复习一下这段代码:

    let range = {
      from: 1,
      to: 5,
    
    	// for...of 会在一开始调用这个方法
      [Symbol.iterator]() {
    	  // 使用扩展运算符(...)返回一个可迭代的对象:
    	  // for...of 循环只使用这个可迭代对象并通过next方法获取值
        return {
          current: this.from,
          last: this.to,
    
          // for..of 每次循环都会调用next()
          next() {
            // 返回对象 {done:.., value :...}
            if (this.current <= this.last) {
              return { done: false, value: this.current++ };
            } else {
              return { done: true };
            }
          }
        };
      }
    };
    
    // 迭代器超出范围后将会返回从 range.from 到 range.to 之间的值
    alert([...range]); // 1,2,3,4,5
    

    我们也可以用 Generator 函数来定义 Symbol.iterator 方法,下面是一个相同范围但是更简洁的例子:

    let range = {
    	from: 1,
    	to: 5,
    
    	*[Symbol.iterator]() { // 简写版的 [Symbol.iterator]: function*()
    		for(let value = this.from; value <= this.to; value++) {
    			yield value;
    		}
    	}
    };
    
    alert( [...range] ); // 1,2,3,4,5
    

    它的作用原理是 range[Symbol.iterator]() 现在返回了一个 Generator,Generator 方法正好满足了 for…of 迭代时需要的:

    • .next() 方法
    • 返回这种形式的结果: {value: …, done: true/false}

    当然这不是一个巧合,Generator 在设计加入 JavaScript 时就考虑到了迭代器的思想,这才让我们实现起来如此容易。

    Generator 在保持原有的 range 功能的同时,比原有的代码显得简洁很多。

    Generator 组合

    Generator 组合是它的一个特殊功能,它允许我们显式地在 Generator 中「嵌入」Generator。

    例如,我们有一个生成序列数字的函数:

    function* generateSequence(start, end) {
      for (let i = start; i <= end; i++) yield i;
    }
    

    我们希望可以复用它来生成更多复杂序列:

    • 首先,生成数字 0..9(字符编码 48..57
    • 然后,生成大写字母 A..Z(字符编码65..90
    • 最后,生成小写字母 a..z(字符编码 97..122

    我们可以用这些字符来创建一个密码(也可以使用特殊字符),我们先生成出来。

    在普通函数中,如果要结合其他函数的结果,我们可以调用外部函数,存储结果,最后加入到返回值中。

    使用 Generator 的话,我们可以利用它的特殊语法 yield* 来「嵌入」(混合)其他Generator 函数。

    一个组合的 Generator:

    function* generateSequence(start, end) {
      for (let i = start; i <= end; i++) yield i;
    }
    
    function* generatePasswordCodes() {
    
      // 0..9
      yield* generateSequence(48, 57);
    
      // A..Z
      yield* generateSequence(65, 90);
    
      // a..z
      yield* generateSequence(97, 122);
    
    }
    
    let str = '';
    
    for(let code of generatePasswordCodes()) {
      str += String.fromCharCode(code);
    }
    
    alert(str); // 0..9A..Za..z
    

    yield* 指令会委托其他 Generator 函数来执行, yield* gen 会遍历 Generator 函数 gen,然后显式地依次输出它的 yield 值。看起来像是外层的 Generator 函数直接输出了内部的 yield 一样。

    把遍历的过程加进来,相当于下面这样:

    function* generateSequence(start, end) {
      for (let i = start; i <= end; i++) yield i;
    }
    
    function* generateAlphaNum() {
    
      // yield* generateSequence(48, 57);
      for (let i = 48; i <= 57; i++) yield i;
    
      // yield* generateSequence(65, 90);
      for (let i = 65; i <= 90; i++) yield i;
    
      // yield* generateSequence(97, 122);
      for (let i = 97; i <= 122; i++) yield i;
    
    }
    
    let str = '';
    
    for(let code of generateAlphaNum()) {
      str += String.fromCharCode(code);
    }
    
    alert(str); // 0..9A..Za..z
    

    Generator 组合天然就适合把 Generator 流引入到其他 Generator 中,不需要额外使用内存去储存中间结果。

    “yield”是双向的

    到目前为止,Generator 和可迭代对象很相似,只是用了特殊的语法。事实上他们要更加强大和灵活。

    这是因为 yield 是双向的: 不仅向外部返回结果,还可以在内部传递参数。

    为了验证,我们传递一个参数来调用 generator.next(arg),这个参数会变成 yield的结果。

    function* gen() {
      // 传出一个问题,等待回答
      let result = yield "2 + 2 = ?"; // (*)
    
      alert(result);
    }
    
    let generator = gen();
    
    let question = generator.next().value; // <-- yield returns the value
    
    generator.next(4); // --> pass the result into the generator
    

    理解 Generator

    1. 第一次调用generator.next()不需要传参数(传了也会被忽略)。它会开始执行并返回第一个 yield “2+2=?”。这时函数暂停执行,停在 (*) 这行。
    2. 然后,yield 返回的值赋值给了 question变量。
    3. generator.next(4),Generator 恢复执行,4作为结果被引入到了 let result = 4

    请注意,外部的代码并不需要立即调用 next(4),隔段时间也没关系,Generator 会等着。

    例如:

    // resume the generator after some time
    setTimeout(() => generator.next(4), 1000);
    

    我们可以发现,和普通函数不一样的是,Generator 可以通过 next/yield 来传递参数,从而交换结果。

    用另一个例子看得更清楚:

    function* gen() {
      let ask1 = yield "2 + 2 = ?";
    
      alert(ask1); // 4
    
      let ask2 = yield "3 * 3 = ?"
    
      alert(ask2); // 9
    }
    
    let generator = gen();
    
    alert( generator.next().value ); // "2 + 2 = ?"
    
    alert( generator.next(4).value ); // "3 * 3 = ?"
    
    alert( generator.next(9).done ); // true
    

    执行过程:

    理解 Generator

    1. 第一个 .next()开始执行,到第一个 yield
    2. 外层代码返回结果
    3. 第二个 next(4)4作为第一个 yield的结果传进去
    4. 到第二个 yield, 正常执行 Generator 的调用
    5. 第三个 next(9)9 作为第二个 yield的结果传进去,继续执行到结束,所以 done: true

    这有点像一个乒乓球游戏,每一个 next(value) (除了第一个)都传递了一个值给 Generator, 然后变成了当前 yield 的结果,再又开始下一个 yield

    generator.throw

    上面的例子可以发现,外层代码可以把值传到 Generator 中作为 yield 的结果。

    但是仍然可能会报错,这很正常,错误也是一种结果。

    如果传一个错误给 yield, 我们可以调用 generator.throw(err),在执行到 yield这行时就会抛出错误err

    例如,这里yield "2 + 2 = ?" 导致了错误。

    function* gen() {
      try {
        let result = yield "2 + 2 = ?"; // (1)
    
        alert("The execution does not reach here, because the exception is thrown above");
      } catch(e) {
        alert(e); // shows the error
      }
    }
    
    let generator = gen();
    
    let question = generator.next().value;
    
    generator.throw(new Error("The answer is not found in my database")); // (2)
    

    (2) 这一行传入的错误,导致了 (1) 这一行执行异常。这里我们使用了 try...catch来捕获了错误。

    如果我们不捕获它,那么就会像普通异常一样,在调用的地方被抛出。

    当前调用 generator.throw的是 (2)这行,所以我们可以在这里捕获到它,就相当于:

    function* generate() {
      let result = yield “2 + 2 = ?”; // Error in this line
    }
    
    let generator = generate();
    
    let question = generator.next().value;
    
    try {
      generator.throw(new Error(“The answer is not found in my database”));
    } catch(e) {
      alert(e); // shows the error
    }
    

    如果这里没有捕获到错误,那么一般会在更外层的调用代码中被抛出,如果始终没被捕获,那么就会终止脚本。

    总结

    • Generator 是由Generator 函数 function* f(…) {…} 创建的
    • 在 Generator 中存在 yield 运算符
    • 外层代码可以通过 next/yield和 Generator 内部交互结果

    在现代 JavaScript 中,Generator 很少被应用。但是它通过函数调用来交换数据的方式很特别,在创建可迭代对象方面也很方便。

    下一章我们将学习异步 Generator,在 for await … of中读取异步产生的数据流。

    在网页程序中,我们经常使用数据流,所以也是非常有必要学习的。

    任务练习

    伪随机 Generator

    我们常常会碰到一些生成随机数据的需求,比如测试一个随机值是否能输出正常结果。

    在JavaScript 中,我们可以使用 Math.random() 来实现随机,但如果用于测试中,我们希望出现异常的数据是可以复现的,所以有时候需要生成一些伪随机数据。

    「伪随机」的意思是,如果开始值(种子)不变,那么后面的随机序列也不变。这个种子也叫「随机种子」,它传入第一个数据,然后根据公式生成下一个。整个过程是可以复现的,只需要传入相同的种子即可。

    例如我们想下面的公式来生成一组比较分散的值:

    next = previous * 16807 % 2147483647
    

    假设我们用1来作为种子,将生成以下值:

    1. 16807
    2. 282475249
    3. 1622650073
    4. …等等…

    现在给你一个任务,创建一个 Generator 函数 pseudoRandom(seed),获取 seed 值然后用公式生成结果。

    用例如下:

    let generator = pseudoRandom(1);
    
    alert(generator.next().value); // 16807
    alert(generator.next().value); // 282475249
    alert(generator.next().value); // 1622650073
    

    答案:

    ...

    ...

    ...

    ...

    ...

    第一种方法:

    function* pseudoRandom(seed) {
      let value = seed;
    
      while(true) {
        value = value * 16807 % 2147483647
        yield value;
      }
    
    };
    
    let generator = pseudoRandom(1);
    
    alert(generator.next().value); // 16807
    alert(generator.next().value); // 282475249
    alert(generator.next().value); // 1622650073
    

    第二种方法:使用普通函数来实现,只是不能使用 for..of来迭代,并且无法用到组合 Generator。

    function pseudoRandom(seed) {
      let value = seed;
    
      return function() {
        value = value * 16807 % 2147483647;
        return value;
      }
    }
    
    let generator = pseudoRandom(1);
    
    alert(generator()); // 16807
    alert(generator()); // 282475249
    alert(generator()); // 1622650073
    

    参考内容: javascript.info/generators


    起源地下载网 » 理解 Generator

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元