JavaScript学习之ES6-genertor知识学习(特性与用法)

73 篇文章 1 订阅
19 篇文章 0 订阅

Generator

function* foo() {
  yield "result1";
  yield "result2";
  yield "result3";
}

const gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

ES5 下如何实现 generator

"use strict";

var _marked =
  /*#__PURE__*/
  regeneratorRuntime.mark(foo);

function foo() {
  return regeneratorRuntime.wrap(function foo$(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        case 0:
          _context.next = 2;
          return "result1";

        case 2:
          _context.next = 4;
          return "result2";

        case 4:
          _context.next = 6;
          return "result3";

        case 6:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

var gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

regeneratorRuntime.mark 和 regeneratorRuntime.wrap,这两者其实是 regenerator-runtime 模块里的两个方法,regenerator-runtime 模块来自 facebook 的 regenerator 模块,完整代码在runtime.js

regeneratorRuntime.mark()

regeneratorRuntime.mark(foo)这个方法在第一行被调用,我们先看一下 runtime 里 mark()方法的定义

runtime.mark = function(genFun) {
  genFun._proto_ = GeneratorFunctionPrototype;
  genFUn.prototype = Object.create(Gp);
  return genFun;
};

mark()方法为生成器函数(foo)绑定了一系列原型

regeneratorRuntime.wrap()

babel 转化的代码,执行 foo(),其实是执行 wrap()

function wrap(innerFn, outerFn, self) {
  var generator = Object.create(outerFn.prototype);
  var context = new Context([]);
  generaor._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}

wrap 方法先是创建了一个 generator,并继承 outerFn.prototype;然后 new 了一个 context 对象;makeInvokeMethod 方法接收 innerFn(对应 foo$)、context 和 this,并把返回值挂到 generator._invoke 上;最后 return 了 generator。其实 wrap()相当于是给 generator 增加了一个_invoke 方法

::: tip
outerFn.prototype 其实就是 genFun.prototype,
context 可以直接理解为这样一个全局对象,用于储存各种状态和上下文:
:::

var ContinueSentinel = {};

var context = {
    done:false,
    method:'next',
    next:0,
    prev:0,
    abrupt:function(type,arg){
        var record ={};
        record.type = type;
        record.arg = arg;

        return this.complete(record);
    },
    complete:funtion(record,afterLoc){
        if(record.type === 'return'){
            this.rival = this.arg = record.arg;
            this.method = 'return';
            this.next = 'end';
        }
        return ContinueSentinel;
    },
    stop:function(){
        this.done = true;
        return this.rival;
    }
};

::: tip
makeInvokeMethod 的定义如下,它 return 了一个 invoke 方法,invoke 用于判断当前状态和执行下一步,其实就是我们调用的 next()
:::

//以下是编译后的代码
function makeInvokeMethod(innerFn, context) {
  // 将状态置为start
  var state = "start";

  return function invoke(method, arg) {
    // 已完成
    if (state === "completed") {
      return { value: undefined, done: true };
    }

    context.method = method;
    context.arg = arg;

    // 执行中
    while (true) {
      state = "executing";

      var record = {
        type: "normal",
        arg: innerFn.call(self, context), // 执行下一步,并获取状态(其实就是switch里边return的值)
      };

      if (record.type === "normal") {
        // 判断是否已经执行完成
        state = context.done ? "completed" : "yield";

        // ContinueSentinel其实是一个空对象,record.arg === {}则跳过return进入下一个循环
        // 什么时候record.arg会为空对象呢, 答案是没有后续yield语句或已经return的时候,也就是switch返回了空值的情况(跟着上面的switch走一下就知道了)
        if (record.arg === ContinueSentinel) {
          continue;
        }
        // next()的返回值
        return {
          value: record.arg,
          done: context.done,
        };
      }
    }
  };
}

::: tip

为什么 generator._invoke 实际上就是 gen.next 呢,因为在 runtime 对于 next()的定义中,next()其实就 return 了_invoke 方法
:::

// Helper for defining the .next, .throw, and .return methods of the
// Iterator interface in terms of a single ._invoke method.
function defineIteratorMethods(prototype) {
  ["next", "throw", "return"].forEach(function(method) {
    prototype[method] = function(arg) {
      return this._invoke(method, arg);
    };
  });
}

defineIteratorMethods(Gp);

低配实现 & 调用流程分析

function gen$(_context) {
  while (1) {
    switch ((_context.prev = _context.next)) {
      case 0:
        _context.next = 2;
        return "result1";

      case 2:
        _context.next = 4;
        return "result2";

      case 4:
        _context.next = 6;
        return "result3";

      case 6:
      case "end":
        return _context.stop();
    }
  }
}

// 低配版context
var context = {
  next: 0,
  prev: 0,
  done: false,
  stop: function stop() {
    this.done = true;
  },
};

// 低配版invoke
let gen = function() {
  return {
    next: function() {
      value = context.done ? undefined : gen$(context);
      done = context.done;
      return {
        value,
        done,
      };
    },
  };
};

// 测试使用
var g = gen();
g.next(); // {value: "result1", done: false}
g.next(); // {value: "result2", done: false}
g.next(); // {value: "result3", done: false}
g.next(); // {value: undefined, done: true}

调用流程:

  1. 我们定义的 function*生成器函数被转化为以上代码

  2. 转化后的代码分为三大块:

    • gen$(_context)由 yield 分割生成器函数代码而来
    • context 对象用于储存函数执行上下文
    • invoke()方法定义 next(),用于执行 gen$(_context)来跳到下一步
  3. 当我们调用 g.next(),就相当于调用 invoke()方法,执行 gen$(_context),进入 switch 语句,switch 根据 context 的标识,执行对应的 case 块,return 对应结果

  4. 当生成器函数运行到末尾(没有下一个 yield 或已经 return),switch 匹配不到对应代码块,就会 return 空值,这时 g.next()返回{value: undefined, done: true}

从中我们可以看出,Generator 实现的核心在于上下文的保存,函数并没有真的被挂起,每一次 yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个 context 对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样

Generator 实现对象属性 Iteration

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }

  let people = { first: "three", last: "s" };

  for (let [key, value] of objectEntries(people)) {
    console.log(`${three},${value}`);
  }
}
// first : three
// last : s

另一种对象的遍历写法:Symbol.iterator

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }

  let people = { first: "three", last: "s" };

  people[Symbol.iterator] = objectEntries;

  for (let [key, value] of people) {
    console.log(`${three},${value}`);
  }
}
// first : three
// last : s

generator 遍历

ES6 提供了 yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。

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

// 等同于
function* bar() {
  yield "x";
  yield "a";
  yield "b";
  yield "y";
}

// 等同于
function* bar() {
  yield "x";
  for (let v of foo()) {
    yield v;
  }
  yield "y";
}

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

function* inner() {
  yield "hello!";
}

function* outer1() {
  yield "open";
  yield inner();
  yield "close";
}

var gen = outer1();
gen.next().value; // "open"
gen.next().value; // 返回一个遍历器对象
// inner {<suspended>}
// __proto__: Generator
// [[GeneratorLocation]]: VM43:1
// [[GeneratorState]]: "suspended"
// [[GeneratorFunction]]: ƒ* inner()
// [[GeneratorReceiver]]: Window
// [[Scopes]]: Scopes[2]
gen.next().value; // "close"

function* outer2() {
  yield "open";
  yield* inner();
  yield "close";
}

var gen = outer2();
gen.next().value; // "open"
gen.next().value; // "hello!"
gen.next().value; // "close"

yield*后面的 Generator 函数(没有 return 语句时),等同于在 Generator 函数内部,部署一个 for…of 循环。

function* concat(iterator1, iterator2) {
  yield* iterator1;
  yield* iterator2;
}

// 等价于

function* concat(iterato1, iterator2) {
  for (var value of iterator1) {
    yield value;
  }
  for (var value of iterator2) {
    yield value;
  }
}

上面代码说明,yield后面的 Generator 函数(没有 return 语句时),不过是 for…of 的一种简写形式,完全可以用后者替代前者。反之,在有 return 语句时,则需要用 var value = yield iterator 的形式获取 return 语句的值。

如果 yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。

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

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

上面代码中,yield 命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。

实际上,任何数据结构只要有 Iterator 接口,就可以被 yield*遍历。

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

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

上面代码中,yield 表达式返回整个字符串,yield语句返回单个字符。因为字符串具有 Iterator 接口,所以被 yield遍历。

如果被代理的 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 提供了返回值。

function* genFuncWithReturn() {
  yield "a";
  yield "b";
  return "The result";
}
function* logReturned(genObj) {
  let result = yield* genObj;
  console.log(result);
}

[...logReturned(genFuncWithReturn())];
// The result
// 值为 [ 'a', 'b' ]

上面代码中,存在两次遍历。第一次是扩展运算符遍历函数 logReturned 返回的遍历器对象,第二次是 yield*语句遍历函数 genFuncWithReturn 返回的遍历器对象。这两次遍历的效果是叠加的,最终表现为扩展运算符遍历函数 genFuncWithReturn 返回的遍历器对象。所以,最后的数据表达式得到的值等于[ ‘a’, ‘b’ ]。但是,函数 genFuncWithReturn 的 return 语句的返回值 The result,会返回给函数 logReturned 内部的 result 变量,因此会有终端输出。

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

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

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

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

// // [...iterTree(tree)] // ["a", "b", "c", "d", "e"]

使用 yield*语句遍历完全二叉树。

// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
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']

generator 实现对象属性

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

generator 应用

(1) 异步操作

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

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();

读取本地文件

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while (!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

(2) 控制流管理

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(
    function(value4) {
      // Do something with value4
    },
    function(error) {
      // Handle any error from step1 through step4
    }
  )
  .done();

上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

然后,使用一个函数,按次序自动执行所有步骤。

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函数未结束,就继续调用
  if (!taskObj.done) {
    task.value = taskObj.value;
    scheduler(task);
  }
}

注意,上面这种做法,只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。如果要控制异步的操作流程,详见后面的《异步操作》一章。

下面,利用 for…of 循环会自动依次执行 yield 命令的特性,提供一种更一般的控制流管理的方法。

let steps = [step1Func, step2Func, step3Func];

function* iterateSteps(steps) {
  for (var i = 0; i < steps.length; i++) {
    var step = steps[i];
    yield step();
  }
}

上面代码中,数组 steps 封装了一个任务的多个步骤,Generator 函数 iterateSteps 则是依次为这些步骤加上 yield 命令。

将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。

let jobs = [job1, job2, job3];

function* iterateJobs(jobs) {
  for (var i = 0; i < jobs.length; i++) {
    var job = jobs[i];
    yield* iterateSteps(job.steps);
  }
}

(3) 部署 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

上述代码中,myObj 是一个普通对象,通过 iterEntries 函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署 next 方法。

下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。

function* makeSimpleGenerator(array) {
  var nextIndex = 0;

  while (nextIndex < array.length) {
    yield array[nextIndex++];
  }
}

var gen = makeSimpleGenerator(["yo", "ya"]);

gen.next().value; // 'yo'
gen.next().value; // 'ya'
gen.next().done; // true

参考

  1. Generaotr 实现
  2. ECMAScript6 入门
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ES6-ES12是JavaScript的不同版本,每个版本都引入了新的特性和改进。以下是一些ES6到ES12的新特性的示例: ES6(2015年): 1. 箭头函数:简化了函数的语法。 2. 模板字面量:使用反引号(`)来创建多行字符串和插入变量。 3. 解构赋值:从对象或数组中提取值并赋给变量。 4. let和const:引入块级作用域的变量声明方式。 5. Promise:处理异步操作的更强大的方式。 ES7(2016年): 1. Array.prototype.includes():判断数组是否包含某个元素。 2. 指数操作符:使用双星号(**)进行指数运算。 ES8(2017年): 1. async/await:更简洁地处理异步操作。 2. Object.values()和Object.entries():从对象中提取值或键值对。 ES9(2018年): 1. Rest/Spread属性:通过...语法来处理函数参数和对象字面量。 2. Promise.prototype.finally():在Promise结束时执行操作。 ES10(2019年): 1. Array.prototype.flat()和Array.prototype.flatMap():用于处理嵌套数组的方法。 2. Object.fromEntries():将键值对列表转换为对象。 ES11(2020年): 1. 可选链操作符(Optional chaining):简化了访问深层嵌套属性的方式。 2. Nullish coalescing操作符:提供了一种默认值的设定方式。 ES12(2021年): 1. Promise.any():在多个Promise中返回最快解决的结果。 2. Logical Assignment Operators:提供了逻辑运算符与赋值的结合方式。 当然,以上仅是一些主要的新特性ES6-ES12还有其他许多有用的功能和语法改进。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值