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 数据处理
在JavaScript中,异步函数是指什么
【5月更文挑战第9天】JavaScript中的异步函数用于处理非立即完成的操作,如定时器、网络请求等。它们可通过回调函数、Promise或async/await来实现。示例展示了如何使用async/await模拟网络请求:定义异步函数fetchData返回Promise,在另一异步函数processData中使用await等待结果并处理。当fetchData的Promise解析时,data变量接收结果并继续执行后续代码。注意,调用异步函数不会阻塞执行,而是会在适当时间点继续。
7 0
|
1天前
|
自然语言处理 JavaScript 前端开发
在JavaScript中,this关键字的行为可能会因函数的调用方式而异
【5月更文挑战第9天】JavaScript中的`this`关键字行为取决于函数调用方式。在非严格模式下,直接调用函数时`this`指全局对象,严格模式下为`undefined`。作为对象方法调用时,`this`指向该对象。用`new`调用构造函数时,`this`指向新实例。通过`call`、`apply`、`bind`可手动设置`this`值。在回调和事件处理中,`this`可能不直观,箭头函数和绑定方法可帮助管理`this`的行为。
8 1
|
1天前
|
JavaScript 前端开发 网络架构
JavaScript中的箭头函数是一种新的函数表达形式
【5月更文挑战第9天】JavaScript的箭头函数以简洁语法简化函数定义,其特性包括:1) 不绑定自身this,继承上下文的this,适合回调和事件处理;2) 没有arguments对象,需用剩余参数语法访问参数;3) 不能用作构造函数,无new调用;4) 没有prototype属性,不支持基于原型的继承。箭头函数在特定场景下优化了this处理,但使用时要注意与普通函数的差异。
6 2
|
4天前
|
JavaScript 前端开发
js的一些内置函数
js的一些内置函数
7 1
|
4天前
|
JavaScript 前端开发 索引
js的includes函数
js的includes函数
9 1
|
4天前
|
JavaScript 安全 前端开发
js的map函数
js的map函数
7 0
|
4天前
|
JavaScript 前端开发
js的filter函数
js的filter函数
8 1
|
4天前
|
JavaScript 前端开发
js的函数
js的函数
6 0
|
4天前
|
JavaScript 前端开发
js的join函数
js的join函数
7 1
|
4天前
|
JavaScript 前端开发
js的check函数
js的check函数
8 1