JavaScript中 Generator 函数详解(1)

简介: JavaScript中 Generator 函数详解(1)

1. 简介

Generator 函数是 ES6 提供的一种异步编程解决方案。它既是一个生成器,也是一个状态机,内部拥有值及相关的状态,生成器返回一个迭代器 Iterator 对象,可以通过这个迭代器,手动地遍历相关的值、状态,保证正确的执行顺序。

特征:

  • function 关键字和函数之间有一个星号(*),且内部使用 yield 表达式,定义不同的内部状态;
  • 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object);

function* gen() { 
  yield 1;
  yield 2;
  return 3;
  yield 4;
}
let g = gen(); 
console.log(g.next());   // {value: 1, done: false}
console.log(g.next());   // {value: 2, done: false}
console.log(g.next());   // {value: 3, done: true}
console.log(g.next());  // {value: undefined, done: true}

每次调用 next() 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next() 方法可以恢复执行。

调用 Generator 函数,返回一个遍历器对象 (Iterator),代表 Generator 函数的内部指针。每次调用遍历器对象的 next() 方法,就会返回一个有着 value 和 done 两个属性的对象。value 属性表示当前的内部状态的值,是 yield 表达式后面那个表达式的值;done 属性是一个布尔值,表示是否遍历结束。

Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 语句里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 语句下面,反正要等到调用 next() 方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作 改写回调函数。

注意: 如果 return 语句后面还有 yield 表达式,那么后面的 yield 完全不生效。

2. 与 Iterator 接口的关系

任意一个对象的 Symbol.iterator 方法,是一个遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。

Iterator 的 return 的值不会被 for…of 循环到 , 也不会被扩展符遍历到

function* gen() { 
  yield 1;
  yield 2;
  return 3;
}
let g = gen();
console.log([...g]);  // [1, 2]
var obj = {};
obj[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  return 3;
};
for(let foo of [...obj]) {
  console.log(foo) // 1, 2
}
console.log([...obj]); // [1, 2]

3. yield 表达式

由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield 表达式就是暂停标志。

迭代器对象的 next 方法的运行逻辑如下:

下一次调用 next() 方法时,再继续往下执行,直到遇到下一个 yield 表达式;

如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值;

如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined;

注意:

yield 语句只能用于 function* 的作用域,如果 function* 的内部还定义了其他的普通函数,则函数内部不允许使用 yield 语句;

yield 语句如果参与运算,必须用括号括起来;

function* gen1() {
  yield  123 + 456;    // yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值
};
const g1 = gen1(); 
console.log(g1.next()); // { value: 579, done: false }
function* gen2() {
  return function () {
    yield 1   // SyntaxError 语法错误
  }
};
'Hello' + yield 'world'; // SyntaxError 语法错误
'Hello' + (yield 'world'); 

4. next() 方法的参数

  • yield 表达式本身没有返回值,或者说总是返回 undefined;
  • next() 方法可以带一个参数,该参数会改变上一个 yield 表达式的返回值;
function* gen(x) {
  const y = 2 * (yield (x + 1));
  const z = yield (y / 2);
  return (x + y + z); 
}        
let a = gen(6);
console.log(a.next());    // {value: 7, done: false}
console.log(a.next());    // {value: NaN, done: false}
console.log(a.next());    // {value: NaN, done: true}
let b = gen(6);
console.log(b.next());    // {value: 7, done: false}
console.log(b.next(10));  // {value: 10, done: false}
console.log(b.next(8));   // {value: 34, done: true}

第一次调用 a 的 next() 方法时,返回 x+1 的值 7。第二次运行 a 的 next() 方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的 value 属性也等于 NaN。第三次运行a的 next() 方法的时候不带参数,所以 z 等于 undefined,返回对象的 value 属性等于 5 + NaN + undefined,即NaN。第一次调用 b 的 next() 方法时,返回 x+1 的值7;第二次调用 next() 方法,将上一次 yield 表达式的值设为10,因此 y 等于20,返回 y / 2 的值 10;第三次调用 next() 方法,将上一次 yield 表达式的值设为 8,因此 z 等于 8,这时 x 等于6,y 等于20,所以 return 语句的值等于 6+20+8。

注意:

  • 在第一次使用 next 方法时,传递参数是无效的;
  • V8 引擎直接忽略第一次使用 next() 方法时的参数,只有从第二次使用next方法开始,参数才是有效的;
  • 从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数;

5. Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};
var i = g();
i.next();
try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

上面代码中,遍历器对象 i 连续抛出两个错误。第一个错误被 Generator 函数体内的 catch 语句捕获。

i 第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的 catch 语句捕获。

throw 方法可以接受一个参数,该参数会被 catch 语句接收,建议抛出 Error 对象的实例。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};
var i = g();
i.next();
i.throw(new Error('出错了!'));
// Error: 出错了!(…)

注意: 不要混淆遍历器对象的 throw 方法和全局的 throw 命令。上面代码的错误,是用遍历器对象的 throw 方法抛出的,而不是用 throw 命令抛出的。后者只能被函数体外的 catch 语句捕获。

如果 Generator 函数内部没有部署 try…catch 代码块,那么 throw 方法抛出的错误,将被外部 try…catch 代码块捕获。

var g = function* () {
  while (true) {
    yield;
    console.log('内部捕获', e);
  }
};
var i = g();
i.next();
try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 a

上面代码中,Generator 函数 g 内部没有部署 try…catch 代码块,所以抛出的错误直接被外部 catch 代码块捕获。

如果 Generator 函数内部和外部,都没有部署 try…catch 代码块,那么程序将报错,直接中断执行。

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined

上面代码中,g.throw 抛出错误以后,没有任何 try…catch 代码块可以捕获这个错误,导致程序报错,中断执行。

throw 方法被捕获以后,会附带执行下一条 yield 表达式。也就是说,会附带执行一次 next 方法。

var gen = function* gen(){
  try {
    yield console.log('a');
  } catch (e) {
    // ...
  }
  yield console.log('b');
  yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c

上面代码中,g.throw 方法被捕获以后,自动执行了一次next方法,所以会打印 b。另外,也可以看到,只要 Generator 函数内部部署了 try…catch 代码块,那么遍历器的 throw 方法抛出的错误,不影响下一次遍历。

一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用 next 方法,将返回一个 value属性等于 undefined、done 属性等于 true 的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}
function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第二次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第三次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done

上面代码一共三次运行 next 方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator 函数就已经结束了,不再执行下去了。

6. Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个 return() 方法,可以返回给定的值,并且终结遍历 Generator 函数。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}
var g = gen();
g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

上面代码中,遍历器对象 g 调用 return() 方法后,返回值的 value 属性就是 return() 方法的参数 foo。

return 的参数值覆盖本次 yield 语句的返回值,并且提前终结遍历,即使后面还有 yield 语句也一律无视。

并且 Generator 函数的遍历就终止了,返回值的 done 属性为 true,以后再调用 next() 方法,done 属性总是返回 true。

如果 return() 方法调用时,不提供参数,则返回值的 value 属性为 undefined。



相关文章
|
1月前
|
JavaScript 前端开发 Java
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
本文介绍了JavaScript中常用的函数和方法,包括通用函数、Global对象函数以及数组相关函数。详细列出了每个函数的参数、返回值及使用说明,并提供了示例代码。文章强调了函数的学习应结合源码和实践,适合JavaScript初学者和进阶开发者参考。
43 2
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
|
1月前
|
前端开发 JavaScript 开发者
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。
|
2月前
|
JavaScript 前端开发
JavaScript 函数语法
JavaScript 函数是使用 `function` 关键词定义的代码块,可在调用时执行特定任务。函数可以无参或带参,参数用于传递值并在函数内部使用。函数调用可在事件触发时进行,如用户点击按钮。JavaScript 对大小写敏感,函数名和关键词必须严格匹配。示例中展示了如何通过不同参数调用函数以生成不同的输出。
|
2月前
|
存储 JavaScript 前端开发
JS函数提升 变量提升
【10月更文挑战第6天】函数提升和变量提升是 JavaScript 语言的重要特性,但它们也可能带来一些困惑和潜在的问题。通过深入理解和掌握它们的原理和表现,开发者可以更好地编写和维护 JavaScript 代码,避免因不了解这些机制而导致的错误和不一致。同时,不断提高对执行上下文等相关概念的认识,将有助于提升对 JavaScript 语言的整体理解和运用能力。
|
3月前
|
JavaScript 前端开发 安全
JavaScript函数详解
JavaScript函数的详细解析,包括函数的定义和调用方式(如一般格式、匿名函数、构造函数、自调用函数、箭头函数和严格模式)、函数参数(arguments对象、可变参数、默认参数值)、闭包的概念和应用实例。
JavaScript函数详解
|
2月前
|
JavaScript 前端开发
js教程——函数
js教程——函数
52 4
|
2月前
|
存储 JavaScript 前端开发
js中函数、方法、对象的区别
js中函数、方法、对象的区别
23 2
|
2月前
|
JavaScript 前端开发 Java
【javaScript数组,函数】的基础知识点
【javaScript数组,函数】的基础知识点
32 5
|
2月前
|
JavaScript 前端开发
Node.js 函数
10月更文挑战第5天
26 3
|
2月前
|
前端开发 JavaScript
探索JavaScript函数基础
探索JavaScript函数基础
23 3