ES6 Generator 函数的语法 yield表达式

Generator 函数的语法

基本介绍

Genertator 函数是ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数有多种理解的角度,语法上, 首先可以把它理解成,Genterator函数是一个状态机,封装了多个内部状态。

执行Generator 函数会返回一个遍历器对象, 也就是说, Genertator 函数除了状态机,还是一个遍历器对象生成函数,返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。

形式上,Generator函数是一个普通函数, 但是有两个特征,一是,function关键字与函数名之间有一个星号;二是, 函数体内部使用yield 表达式 , 定义不同的内部状态。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

上面代码定义了一个Generator 函数 , 它内部有两个yield表达式 ,即刻函数有三个状态:hello , world 和return语句。

然后, Generator 函数的调用方法与普通函数一样, 也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后 ,函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的的一个指针对象,也就是遍历器对象。

下一步,必须调用遍历器对象的next 方法, 使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或者上一次停下来的地方开始执行, 直到遇到下一个yield 表达式,为止,换言之。Generator函数是分段执行的。yield表达式是暂停执行的的标记,而next 方法可以恢复执行。

总结

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

yield 表达式

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

遍历器对象的next 方法的运行逻辑如下

(1) 遇到yield表达式,就暂停执行后面的操作, 并将紧跟在yield 后面的那个表达式的值,作为返回的对象的value属性值。

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

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

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

需要注意的是,yield 表达式后面的表达式,只有当调用next 方法,内部指针指向该语句时才会执行,因此类似于惰性取值。

function* gen() {
  yield  123 + 456;
}

上面的代码中 ,yield 后面的表达式123+456 不会立即求值, 只会在next方法将指针移到这一句是才会求值。

yield表达式和return语句既有相似之处 , 也有区别,相似之处在于, 都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield , 函数暂停执行, 下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能, 一个函数里面只能执行一次return语句, 但是可以执行多次yield表达式。正常的函数只能返回一个值,因为只能执行一次return;generator函数可以返回一系列的值,因为可以有任意多的yield 从另一个角度看, 也可以说Generator生成了一系列的值。

注意 yield 关键字只能用在Generator函数里面,用在其他地方会报错,!!!!!

Generator 函数可以不用yield 表达式,这时就变成了一个单纯的暂缓执行的函数。

function* f() {
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);

上面代码中,函数f如果是普通函数,在为遍历generator赋值时就会执行。但是 , 函数f是一个generator函数, 就变成只有只有调用next 方法时,函数f才会执行。

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  });
};

for (var f of flat(arr)){
  console.log(f);
}

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

其中yield* 表示可以调用Generator函数。上面的代码把一个复杂的数组对象给化解了

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

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

与Iterator接口的关系

上一章说过,任意一个对象的Symbol.iterator方法 , 等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

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

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

[...myIterable] // [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就被重置为这个参数因此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);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

注意,由于next方法的参数表示上一个yield 表达式的返回值, 所以第一次使用next 方法时, 传递参数是无效的,V8引擎直接忽略第一次使用next 方法时的参数,只有从第二次使用next 方法开始,参数才是有效的。从语义上讲,第一个next 方法用来启动遍历器对象,所以不用带有参数。

function* dataConsumer() {
  console.log('Started');
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return 'result';
}

let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b

上面的代码中${} 表示字符串的拼接,类似与占位符。

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 循环之中。

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

for 循环无限循环 , 每次遇到yield 就返回,因为for循环是无限循环 ,所以第一次是1 , 第二次的时候上一个数加当前这个数,所以第二个数是1 第三个数是2 第四个数是3

利用Genterator 遍历对象

function* objectTest(obj){
	// 获取对象中的所有的属性名,并通过数组返回
	let propkeys = Reflect.ownKeys(obj);
	console.log(propkeys);
	for(let propKey of propkeys){
		yield[propKey , obj[propKey]];
	}
}

let name = {"id":1,"name":'admin'};
for(let [key,value] of objectTest(name)){
	console.log(key,value);
}

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

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 方法内部调用的,都是遍历器接口。这意味着。他们都可以将Gnerator 函数返回的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

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 语句已经执行过了,不会再捕获这个错误了,所以这个错误被抛出了函数体完,被体外的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语句捕获。

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != 'a') throw e;
      console.log('内部捕获', e);
    }
  }
};

var i = g();
i.next();

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 [Error: a]

上面代码之所以捕获了a是因为函数体外的catch 语句块, 捕获了抛出的a错误以后就不会再继续try代码块里面剩余的语句。

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

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 内部没有部署tey…chatch 代码块。所以抛出的错误直接被外部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 方法抛出的错误要被内部捕获, 前提是必须至少在执行一个next方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值