ES6之Generator函数深入学习笔记

ES6之Generator函数深入学习笔记_第1张图片
图片发自App

写在前面的话,说真的ES6的每一章节都超级长啊,这一块的学习我用了两天时间才完成。如果不赶紧把总结写出来,我过一段时间之后又会非常快的忘记掉。所以要赶紧记下来。

  1. 简介
  2. next方法的参数
  3. for...of循环
  4. Generator.prototype.throw()
  5. Generator.prototype.return()
  6. next(),throw(),return()的共同点
  7. yield * 表达式
  8. 作为对象属性的Generator函数
  9. Generator函数的this
  10. 含义
  11. 应用

上面是学习这一章的时候的目录,我们就按这个来总结,争取短点,把事说明白了就行,不明白的可以直接去书,那么我们开始吧。

1. 简介

Generator 函数是 ES6 提供的一种异步编程解决方案.

我们看一下代码:

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

var hw = helloWorldGenerator();

它有多种理解角度,一种是状态机,封装了多个内部状态,但是上面的例子并不好理解。
另一种是遍历器对象生成函数,返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。

它有两个特征。一个是function关键字后面会带一个*号,还有就是函数体内部会使用yield表达式,定义不同的内部状态,(yield在英语里的意思就是"产出")

Generator函数的调用和普通函数一样,但是不同的是,调用后并不会立即得到值,而是返回一个遍历器对象。然后需要通过遍历器对象的next方法来遍历yield后面的表达式。
每一次调用next方法,都会在yield表达式后面暂停下来,然后得到yield表达式后面的值,返回到遍历器对象的value中。然后在重复上面的过程!

Generator函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

1.第一次调用,返回yield后面表达式的值,valuehello,donefalse表示还没有执行完。
2.第二次调用,返回yield后面表达式的值,valueworld,donefalse表示还没有执行完。
3.第三次调用,返回return后面表达式的值,valueending,donetrue表示已经执行完了。
4.第四次调用,valueundefined,donetrue表示执行完了。

总结一下:调用'Generator'函数,返回一个遍历器对象,代表的是Generator函数的内部指针。每次调用遍历器对象的next方法,会返回一个包含valuedone两个属性的对象。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方法,内部执行指向该语句时才会执行。因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

function * gen(){
    yield 123+456;
}
yield表达式与return语句的区别:

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

yield表达式只能在Generator函数中,用在其他地方会报错。
(function(){
    yield 1;
})();
// SyntaxError: Unexpected number

与Iterator接口的关系

Symbol章节中:

任意一个对象的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属性,执行后返回自身。

2.next方法的参数

在这里我们要首先记住一点,yield表达式本身是没有返回值的,或者说返回的是undefined。看到这里我们一定会有疑惑,就是为什么会没有返回值呢,那上面我们写的代码里hw.next().value这不是返回值吗,调用的第一个还返回了一个hello呢,其实这里说的么有返回值不是调用next()方法后没有返回值,而是在代码内部里面内没有,比如说

function * foo(){
    var g = yield 'hello';
    console.log(g);
}

在这个例子里,我们的函数执行完next方法以后,暂停在yield函数体后面,g并没有被我们赋值,当我们调用第二次next方法的时候,也就是从上一次暂停地方开始执行的时候,如果我们不把上一个next的函数调用完返回值传递回去,让它把这个值赋值给g的话,那么g就是undefined,所以这里引入了next参数,就是为了给暂停以后,传值用的。

理解了上面就可以了!

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 }

看一下,为什么会输出这样的结果呢??

3. 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循环依次遍历5个yield表达式的值。但是,它不会遍历return的值,在遍历的过程中,如果碰到done属性为true时,表明已经完成,那么它的遍历就会结束,并且不会反悔done属性为true的对象。所以只打印了1~5,没有6.

下面是用Generator函数和for...of循环,实现斐波那契数列的例子。

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

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

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

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

加上遍历器的另一种写法是,将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方法内部调用的,都是遍历器接口。这也意味着,它们可以将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

知识被串联起来的感觉。

4.Generator.prototype.throw()

我们先说一下这个是干嘛的,我们看上面的标题Generator.prototype.throw()就知道,这是一个遍历器对象的方法,和next方法,还有后面要介绍的return方法一样,都是遍历器对象的方法。

它的主要作用是抛出错误,在体外抛出,在体内捕捉。

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调用throw方法抛出错误a,可以看到打印的数据 是函数体内部捕获,由于函数体内部已经执行过捕获错误,所以第二天抛出错误的时候,不在捕获,交给了外部的处理。

如果捕获的错误在内部和外部都没有处理,则程序终止执行。
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代码块可以捕获这个错误,导致程序报错,中断执行。

thorw方法捕获以后,会附带执行下一条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抛出错误以后,b也被打印出来了,就相当于自动执行了一次g.next方法。所以,只要函数体内布置了try...catch方法,那么遍历器对象调用throw方法抛出的错误,就不会影响下一次遍历,影响程序的执行。

throwg.throw方法是无关的,两者互不影响。
var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}

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

try {
  throw new Error();
} catch (e) {
  g.next();
}
// hello
// world

上面代码中,throw命令抛出的错误不会影响到遍历器的状态,所以两次执行next方法,都进行了正确的操作。

记住一点,如果遍历器对象调用throw方法,而函数体内部没有try...catch进行处理,那么就不会在执行下去了,也就是已经执行完成。下一次调用next方法,将返回一个value属性等于undefineddone属性等于true的对象。

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 函数就已经结束了,不再执行下去了。

5. Generator.prototype.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 }

遍历器对象调用retrun方法后,传入了参数foo,所以返回值的value属性就是retrun返回的参数foo.并且Generator函数的遍历终止了,返回值的done属性为true,以后再调用next方法,done属性总是返回true.

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

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return() // { value: undefined, done: true }

如果Generator函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完在执行。

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

上面代码中,调用return方法后,就开始执行finally代码块,然后等到finally代码块执行完,再执行return方法。

6. next()、throw()、return()、的共同点

其实他们三个方法本质上是同一件事,都是遍历器对象的方法,作用都是让Generator函数恢复执行,并且使用不同语句替换yield表达式。

next()是将yield表达式替换成一个值。
throw()是将yield表达式替换成一个throw语句。
return()是将yield表达式替换成一个return语句。

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

7.yield * 表达式

这个表达式用来在Generator函数内部调用另一个Generator函数。看一个例子。

普通的调用:

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  foo();
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "y"

普通调用,没有效果,这里就需要用到yield *表达式,用来一个Generator函数里面调用另一个Generator函数。

let delegatedIterator = (function*(){
    yield 'Hello!';
    yield 'Bye!';
})();

let delegatingIterator = (function*(){
    yield 'Greetings!';
    yield * delegatedIterator;
    yield 'ok,bye!';
})();

for(let value of delegatingIterator){
    console.log(value);
}

// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

这里用到了立即执行函数,就相当于我们直接调用赋值给了delegatedIterator,相当于这个操作var g = numbers();,好,我们在delegatingIterator这个函数中调用了delegatedIterator这个遍历器对象。我们可以看到实际的效果是直接把delegatedIterator中的yield表达式的值打印了出来。

在一个generator函数里通过yield *就相当于在这个函数里通过for...of循环遍历了另一个generator函数体(没有return语句时)。

function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

// 等同于

function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

如果没有returnyield *表达式就是for...of一种简写,完全可以用来后者替代前者。
如果有return语句,则需要用var value = yield * iterator的形式获取return语句的值

实际上yieldb *后面更的表达式,只要这个表达式支持遍历器,只要有Iterator接口,那么就可以遍历,比如数组字符串

数组

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }

字符串

let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"

如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据。

function* foo() {
  yield 2;
  yield 3;
  return "foo";
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}

var it = bar();

it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

上面代码在第四次调用next方法的时候,屏幕上输出了,这是因为foo函数通过return语句,把值给了bar;

yield *可以很方便地取出所有嵌套数组的成员。

function * iterTree(tree){
    if(Array.isArray(tree)){
        for(let i = 0; i < tree.length;i++){
            yield * iterTree(tree);
        }
    }else{
        yiled tree;
    }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

还有遍历二叉树,这些代码都值得自己写一遍。

// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉树
function make(array) {
  // 判断是否为叶节点
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

8.作为对象属性的Generator函数

如果一个对象的属性是Generator函数,可以简写成下面的形式

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

完整形式如下:

let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

9.Generator函数的this

一句话来总结:你不可以把Generator函数当做普通的构造函数。

为什么呢?

Generator函数返回一个遍历器,而这个遍历器对象就是Generator函数的实例,它也继承了Generator函数的prototype对象上的方法。

function* g() {}

g.prototype.hello = function () {
  return 'hi!';
};

let obj = g();

obj instanceof g // true
obj.hello() // 'hi!'

上面代码里,Generator函数g返回一个遍历器对象obj,是g的实例,而且也继承了g.prototype,但是你不能把它当做普通对象,并不会生效,因为它返回的总是遍历器对象,而不是this对象。

function * g(){
    this.a = 11;
}
let obj = g();
obj.next();
obj.a //undefined

上面的代码中,我们给gthis对象上添加了一个属性a,但是obj对象拿不到这个属性。

Generator函数也不能更new命令一起用,会报错。

function* F() {
  yield this.x = 2;
  yield this.y = 3;
}

new F()
// TypeError: F is not a constructor

上面的代码中,new命令更构造函数一起使用,结果报错,因为F不是构造函数。

那么问题来了,我们想让一个Generator函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this?

obj换成F.ptototype

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

再将F改成构造函数,就可以对它执行new命令了。

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

10.含义

Generator与状态机

一个普通的状态机是这样子的

var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}

上面的clock函数有两种状态,TickTock,没运行一次,就改变一次状态,用Generator实现。

var clok = function*(){
    while(true){
        console.log('Tick!');
        yield;
        console.log('Tock!');
        yield
    }
}

上面的状态机比ES5的实现更优雅,更简洁,我们并没有保存状态的外部变量,因为它本身就包含了一个状态信息,也就是目前是否处于暂停状态。这样更加简洁,更安全,状态不会被非法修改。

11.应用

这里介绍了四种应用场景,我们依次来看一下。

(1) 异步操作的同步表达----不需要在写回调函数

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
  hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()

// 卸载UI
loader.next()

可以看到,我们在用同步的写法写异步的操作,而回调的操作直接写在了yield表达式的后面。

看这个例子:

function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}

var it = main();
it.next();

看到这里我有一个疑问,为什么request方法内可以调用it.next(response),我们知道yield表达式是没有返回值的,它的返回值是undefined,如果我们我想让下面的代码能够执行下去,就需要把response当做传递回去,那么为什么可以调到it呢,这个实例在下面,这里你就应该想到Generator函数的执行顺序了,我们实例话以后并不会执行里面的程序,也就是var it = main()并没有执行下面的代码,而是停在 在yield那里,好了,想到这个就明白了。

(2) 控制流管理

这里大家可以看原文,我就不写了!

(3) 部署 Iterator 接口

我们可以在任意对象上部署Iterator接口,遍历任意对象。

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

作为数据结构

Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为
Generator函数可以返回一系列的值,这也意味着它可以对任意表达式,提供类似数组的接口。

function* doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

上面代码就是一次返回三个函数,但是由于使用了Generator函数,导致可以像处理数组那样,处理这三个返回的函数。

for(task of doStuff){
    
}

实际上,我们也可以用数组来模拟这种写法。

function doStuff() {
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

上面的函数,一样也可以用for...of循环处理。Generator使得数据或者操作,具备了类似数组的接口。

终于写完了,我以为会写的简短点,可是发现后来,如果需要把事情说明白,你就得写这么多。还有一些是照搬原来的书的部分,只是添加了一些我自己的理解。写完以后不是说已经完事了,最主要的是,温故而知新

你可能感兴趣的:(ES6之Generator函数深入学习笔记)