【重构基础】学习一下生成器和迭代器

68 阅读7分钟

从一道面试题开始讲

const person = {
    name: 'Simon',
    desc: 'a coder',
}
const [name, desc] = person // TypeError: person is not iterable
console.log(name, desc)

这是一道面试题,里面使用数组解构的方法的去获取person对象里面的namedesc两个属性。但是一个普通的对象是不支持使用数据结构的语法来进行结构的。

出现TypeError的原因,其实是因为普通对象里面是没有迭代器这个属性的,当使用解构赋值 [name, desc] = person 时,JavaScript 引擎会尝试调用 person[Symbol.iterator]() 来获取迭代器即person[Symbol.iterator]是一个undefined。

解构赋值的执行过程

  1. 解析左侧结构:JavaScript 引擎首先解析左侧的结构,以确定需要创建哪些变量。
  2. 获取右侧数据源:然后它获取右侧的数据源,这可以是一个数组或对象。
  3. 映射数据:引擎将右侧数据源中的数据映射到左侧创建的变量中。对于数组,这是通过位置索引;对于对象,这是通过键匹配。
  4. 赋值:完成映射后,引擎将数据源中的值赋给对应的变量。

那么我们可以去给这个person的迭代器属性值进行赋值即可让上面的代码实现其功能

const iteratorFunc = function*(){
    yield this.name
    yield this.desc
    return this
}
Object.defineProperty(person, Symbol.iterator, {
    value: iteratorFunc.bind(person),
    writable: false,
    configurable: true,
})

声明一个迭代器函数,然后通过.bind绑定好this,并且赋值到这个对象的迭代器函数里面,上面的代码就能够正常运行了。但是这样还不够好,因为只是按顺序去输出namedesc两个属性,如果将赋值语句变成const [desc, name] = person,再去console.log(name, desc)就会得到a coder Simon这样的结果。

const iteratorFunc = function*() { 
    const keys = Object.keys(this); // 获取对象的所有可枚举属性键 
    for (const key of keys) { 
        yield this[key]; // 逐一 yield 属性值 
    } 
    return this
};

好吧扯远了,今天要讲的内容是迭代器和生成器

迭代器(Iterator)

遍历器(Iterator),它是一种接口,为各种不同的表示集合的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。

来说说迭代器有什么作用

  • 提供统一的访问机制
  • 使得表示集合的数据结构里面的成员能够按照一定的次序出现
  • for...of提供支持

迭代器是怎么工作的

迭代器的便利过程可以分为下面的四步

  1. 创建一个指针,指向当前对象的起始地址
  2. 第一次去调用next(),指针就会指向当前数据结构中的第一个成员
  3. 下一次调用next(),指针从第一个成员指向下一个成员
  4. 一直调用next(),直到遍历结束

从上面的流程可以看出,迭代器需要一个next方法,next里面有对应数据结构的成员出现的顺序,并且返回的内容是需要带有这个成员的value以及整个是否结束的done

function iterate(arr) {
    let index = 0
    return {
        next: function() {
            if(index < arr.length){
                return {value: arr[i], done: false}
            }
            return {value:undefined, done:true}
        }
    }
}

以上就是一个迭代器的默认接口,它提供了一个next方法,让这个数组里面每一个成员根据它们的索引出现

生成器(Generator)

看到上面的iteratorFunc函数,它跟普通的函数有点不一样,在参数后面会多了一个*,这就是本期要讲的生成器函数。

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。通过生成器函数可以让我们更加灵活的去控制我们函数的执行和暂停。

其实对于生成器有很多个理解的角度,首先在语法上,我们可以将它看成一个状态机,它封装了多个内部的状态。例如最开始写的迭代函数,有两个yield和一个return,这其实就是说明它有三个状态,

调用生成器函数会返回一个迭代器对象

const generate = function* (){
    yield 'React'
    yield 'Vue'
}

const generator = generate()

console.log(generator)// Object [Generator] {}


const first = generator.next()
console.log(first)// { value: 'React', done: false }

const second = generator.next()
console.log(second)// { value: 'Vue', done: false }

const third = generator.next()
console.log(third)// { value: undefined, done: true }

每一次调用next方法,会让生成器的状态跳转到下一个,直到碰到下一个yield才会停下来。为什么生成器函数能够更加灵活的控制函数的执行和暂停,因为生成器函数是一个分段执行的函数,通过yield来使得函数暂停,通过next()来使得函数重新开始执行

next方法的参数

其实yield在一般情况下是只会返回一个undefined的值,只有在关键字后面跟着一个表达式或者一个值它就会在next方法里面获取到这个值。生成器是一个状态机,如果不能在状态转换的过程中不能传入新的值去干预生成的值,那么这个生成器函数则是不变的。

其实next方法可以携带一个参数,来传入这个生成器函数里面,这个参数会被生成器函数当作是上一个yield的返回值。我们来看个例子

const add = function* (start) {
  const a = yield start * 2;
  const b = 2 * (yield a + start);
  return start + a + b;
};

const addGen = add(1);
const first = addGen.next(10);
console.log(first); // { value: 2, done: false }

const second = addGen.next(100);
console.log(second); // { value: 201, done: false }

const third = addGen.next(1000);
console.log(third); // { value: 2101, done: true }

例如这个例子,就可以看到next传入的参数,会被当成yield表达式的返回值。这里的a的值是100,b的值是1000

通过这个例子可以看出这个语法的特性,其实这个语法是非常有意义的,因为在Generator函数从暂停到重新开始执行,它的context其实是不变的,但是可以通过next方法传入的参数,在yield之间的每一个阶段,调整这个生成器函数的行为。

Generator.prototype.return

生成器的原型对象上有一个return方法,我们可以通过它来总结整个生成器函数。

const generator = function* () {
  const a = yield "hello";
  const b = yield "world";
  return a + b;
};
const gen = generator();
const first = gen.next();
console.log(first); // { value: 'hello', done: false }

const second = gen.return("bye");
console.log(second); // { value: 'bye', done: true }

const third = gen.next();
console.log(third); // { value: undefined, done: true }

从上面的例子可以看到,使用return方法之后,生成器函数就停止了遍历,并且后续调用next方法都是返回的done都是为true。

如果在生成器函数里面使用try...finally这个代码块,使用return会使得生成器函数直接进入finally代码块中,并且在finnaly代码块中代码yield完毕之后,再返回return方法中传入的值

const generator = function* () {
  yield 1;
  try {
    yield 2;
  } finally {
    yield 3;
    yield 4;
  }
  yield 5;
};
const gen = generator();
const first = gen.next();
console.log(first); // { value: 1, done: false }

const second = gen.next();
console.log(second); // { value: 2, done: false }

const third = gen.return(6);
console.log(third); // { value: 3, done: false }

const fourth = gen.next();
console.log(fourth); // { value: 4, done: false }

const fifth = gen.next();
console.log(fifth); // { value: 6, done: true }

const sixth = gen.next();
console.log(sixth); // { value: undefined, done: true }

Generator.prototype.throw

使用throw方法,可以让生成器函数内部执行到的try...catch捕获到。

const generator = function* () {
  try {
    yield 1;
  } catch (e) {
    console.log("generator error", e);
  }
};
const gen = generator();
gen.next();

try {
  gen.throw("error1");
  gen.throw("error2");
} catch (e) {
  console.log("outside error", e);
}

// generator error error1
// outside error error2

从上面代码可以看出,第一个throw被generator内部捕获到了,但是try...catch已经执行过了,第二个throw就会让外部的try...catch捕获到。

讲讲next(),return()throw()的共同点

这三个方法的本质其实都是能够推动生成器函数这个状态机的状态转换,并且使用不同的方法来替换yield的表达式。

yield*

yield*的用法其实是在一个生成器函数里面去遍历另一个生成器函数,我们来看个例子

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

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

for (const v of bar()) {
  console.log(v);
}

// x
// a
// b
// y

其实上面的代码就是等价于下面的代码

function* bar() {
  yield "x";
  for(let f of foo())
  yield "y";
}

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

如果需要在生成器中嵌套使用别的生成器,手动写的话,会非常地麻烦,使用yield*可以让代码更加的简洁,易懂。

以上就是生成器和迭代器的相关知识