重学JavaScript高级(十一):你真的了解Iterator(迭代器)-Generator(生成器)么?

Iterator(迭代器)-Generator(生成器)详解

此篇文章主要讲解了Iterator(迭代器)-Generator(生成器),在实际开发中用的不是很多,但是对于理解async/await有一定的帮助,同时对React中的redux库的使用,有很大的帮助

大家在阅读文章的时候,有写错的地方,还请多多指正!

什么是迭代器

  • 使用户 在容器对象上遍访的对象,是用该接口,无需关系对象的内部实现过程

重学JavaScript高级(十一):你真的了解Iterator(迭代器)-Generator(生成器)么?_第1张图片

  • 我们可以明白,迭代器就是可以帮助我们对某个数据结构进行遍历的对象
  • 在JavaScript中,迭代器也是一个具体的对象,需要符合协议
    • 该对象中,应该有一个next方法,定义该方法的要求如下
      • 该方法是无参数或者接收一个参数,并且返回两个对象
      • done(boolean):如果迭代器可以产生序列中的下一个值,则为false;如果迭代完毕,则为true
      • value:迭代器返回的值,done为true时候,可以省略
//在js中,我们知道数组可以遍历
let arr = [1,2,3]
//是因为我们在创建数组的时候,js内部帮我们用了迭代器,生成了可迭代对象
  • 接下来我们就要实现以下,js内部迭代器的工作原理(首先给一个arr数组创建迭代器)
    • 有next方法
    • 该方法返回done和value两个属性
let arr = [1, 2, 3, 4];

//此时iteratorArr就是arr的迭代器
let iteratorArr = {
  index: 0,
  next() {
    //没有遍历完的时候,done为false,value为arr[i]
    if (this.index < arr.length) {
      return { done: false, value: arr[this.index++] };
    } else {
      //遍历完成只会,done为false,value可以省略
      return { done: true };
    }
  },
};

console.log(iteratorArr.next());
console.log(iteratorArr.next());
console.log(iteratorArr.next());
console.log(iteratorArr.next());
console.log(iteratorArr.next());
  • 那么接下来我们要封装一个函数,可以为所有的数组创建迭代器
    • 首先该函数可以接收一个参数:为哪个数组创建
    • 之后一个对象,对象中有next方法
let arr = [1, 2, 3, 4];
function createIteratorArr(arrName) {
  index = 0;
  //返回一个对象,对象中包含next方法,所以可以链式调用
  return {
    next() {
      if (index < arrName.length) {
        return { done: false, value: arrName[index++] };
      } else {
        //遍历完成只会,done为false,value可以省略
        return { done: true };
      }
    },
  };
}

let arr1 = createIteratorArr(arr);

console.log(arr1.next());
console.log(arr1.next());
console.log(arr1.next());

可迭代对象

刚刚我们了解了迭代器的原理,那么什么是可迭代对象呢?

可迭代对象与迭代器是不同的概念

当一个对象实现了iterable protocol协议的时候,就是一个可迭代对象

这个对象要求必须事项@@iterator方法,在代码中我们使用Symbol.iterator访问该属性

  • 首先我们知道,一个普通的对象,是不可以被迭代的,那么我们能否为其创建一个迭代器,去迭代里面的内容呢?
//创建一个迭代器,迭代obj中的num属性
let obj = {num:[1,2,3,]}

let iteratorArr = {
  index: 0,
  next() {
    //没有遍历完的时候,done为false,value为arr[i]
    if (this.index < obj.num.length) {
      return { done: false, value:  obj.num[this.index++] };
    } else {
      //遍历完成只会,done为false,value可以省略
      return { done: true };
    }
  },
};
  • 以上代码实现了obj.num的迭代,但是对象和迭代器是分开的,我们能否将两个合并,让迭代器在对象的内部
let obj = {
  arr: [1, 2, 3, 4],
  //这是迭代器函数固定的命名[Symbol.iterator]
  [Symbol.iterator]() {
    //迭代器本身是一个对象,对象中包含next方法
    return {
      index: 0,
      next() {
        //没有遍历完的时候,done为false,value为arr[i]
        if (this.index < obj.arr.length) {
          return { done: false, value: obj.arr[this.index++] };
        } else {
          //遍历完成只会,done为false,value可以省略
          return { done: true };
        }
      },
    };
  },
};

let objIter = obj[Symbol.iterator]();

console.log(objIter.next());
console.log(objIter.next());
console.log(objIter.next());
  • 通过以上操作,此时obj就是一个可迭代对象,因此可以使用for of进行遍历了
for (const item of obj) {
  console.log(item);
}
//1   2   3   4
  • 而数组本身就是一个可迭代的对象,因此,我们可以用另外一种方式遍历数组
let arr = [1, 2, 3, 4];
//获取迭代器
let arrIter = arr[Symbol.iterator]();
console.log(arrIter.next());
console.log(arrIter.next());
console.log(arrIter.next());
/**{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false } */

迭代对象的优化

  • 上面我们将 obj制作成了可迭代对象,那么我们能否对其进行优化
  • 上面我们直接用的 obj.arr,如果对象使用了别的名字,那么就需要进行更改
    • 那么我们就可以想到使用this
    • [Symbol.iterator]函数是被obj调用,所以在[Symbol.iterator]函数的内部this就是obj
    • 而next是被[Symbol.iterator]函数调用的,所以next内部的this指向就是[Symbol.iterator]函数
    • 两种解决方案:将 [Symbol.iterator]传递到next中;将next变成箭头函数
//将 ``[Symbol.iterator]``传递到next中;
let obj = {
  arr: [1, 2, 3, 4],
  //这是迭代器函数固定的命名[Symbol.iterator]
  [Symbol.iterator]() {
    const _this = this;
    //迭代器本身是一个对象,对象中包含next方法
    return {
      index: 0,
      next() {
        //没有遍历完的时候,done为false,value为arr[i]
        if (this.index < _this.arr.length) {
          return { done: false, value: _this.arr[this.index++] };
        } else {
          //遍历完成只会,done为false,value可以省略
          return { done: true };
        }
      },
    };
  },
};
for (const item of obj) {
  console.log(item);
}


//将next变成箭头函数
let obj = {
  arr: [1, 2, 3, 4],
  //这是迭代器函数固定的命名[Symbol.iterator]
  [Symbol.iterator]() {
    //迭代器本身是一个对象,对象中包含next方法
    let index = 0;
    return {
      next: () => {
        //没有遍历完的时候,done为false,value为arr[i]
        if (index < this.arr.length) {
          return { done: false, value: this.arr[index++] };
        } else {
          //遍历完成只会,done为false,value可以省略
          return { done: true };
        }
      },
    };
  },
};
for (const item of obj) {
  console.log(item);
}
  • 那么要对键值对进行遍历,该怎么操作
let object = {
  name: "zhangcheng",
  age: 18,
  [Symbol.iterator]() {
    //可以遍历keys单独获取键
    let keys = Object.keys(this);
    //可以遍历values单独获取值
    let values = Object.values(this);
    let index = 0;
    //使用entries可以获取键值对
    let keyValues = Object.entries(this);
    const iterator = {
      next: () => {
        if (index < keys.length) {
          return { done: false, value: keyValues[index++] };
        } else {
          return { done: true };
        }
      },
    };
    return iterator;
  },
};
for (const iterator of object) {
  console.log(iterator);
}

原生可迭代对象

String Array Map Set argument对象 NodeList集合都是可迭代对象

可迭代对象应用

  • 在JS的特定语法中
    • for…of 展开语法 yield 解构赋值
//展开语法这里需要特别说明一下
let obj = {a:100}
let obj2 = {...obj}//这种是浏览器做了特殊处理,在ES8左右新增的,可以在创建对象字面量的时候使用展开运算符

function foo(a,b){
    
}
foo(...obj)//在这里运用展开运算符的时候就会报错
  • 在创建一些对象的时候
    • new Map new WeakMap new Set new WeakSet
  • 一些方法的调用
    • Promise.all Promise.race Array.from

自定义类的迭代

创建一个类,由这个类创建的所有对象都是可以迭代的

class Person {
  constructor(name, age, friends) {
    this.name = name;
    this.age = age;
    this.friends = friends;
  }
  //在实例对象上创建实例方法
  [Symbol.iterator]() {
    let index = 0;
    //对对象的key进行迭代
    let keys = Object.keys(this);
    const iterator = {
      next: () => {
        if (index < keys.length) {
          return { done: false, value: keys[index++] };
        } else {
          return { done: true };
        }
      },
    };
    return iterator;
  }
}

let p = new Person("zhangcheng", 18, "jame");

for (const iterator of p) {
  console.log(iterator);
}

迭代器的中断

当使用for循环遍历的时候,其内部使用了break、return、throw等中断了遍历,可以在迭代器中使用return函数,监听此类操作

class Person {
  constructor(name, age, friends) {
    this.name = name;
    this.age = age;
    this.friends = friends;
  }
  //在实例对象上创建实例方法
  [Symbol.iterator]() {
    let index = 0;
    //对对象的key进行迭代
    let keys = Object.keys(this);
    const iterator = {
      next: () => {
        if (index < keys.length) {
          return { done: false, value: keys[index++] };
        } else {
          return { done: true };
        }
      },
        //终止的时候,默认执行return函数
      return: () => {
        console.log("终止了");
          //函数内部需要返回一个对象,否则会报错
        return { done: true };
      },
    };
    return iterator;
  }
}

let p = new Person("zhangcheng", 18, "jame");

for (const iterator of p) {
  break;
}

什么是生成器

学习生成器,对于我们理解async/await的理解会有帮助;同时对于React中的redux库的使用,也会有很大的帮助

生成器是ES6新增的一种函数控制使用的方案,可以让我们灵活的控制函数什么时候继续执行、暂停执行

生成器是特殊的迭代器

  • 生成器也是一个函数,但是和普通的函数有区别
    • 首先,生成器函数需要在 **function关键字后面加 ***
    • 生成器可以通过yield来控制函数的暂停
    • 生成器函数默认执行的时候,返回的是一个生成器对象

生成器函数的基本使用

  • 生成器函数返回的是生成器对象
  • 调用生成器对象的next()方法,会真正执行函数
  • 遇到 yield 就会暂停
function* foo() {
  console.log(111);
  console.log(222);
  yield;
  console.log(333);
  yield;
  console.log(444);
  console.log(555);
  console.log(666);
  console.log(777);
}

const generator = foo();
generator.next(); //111  222
generator.next(); //111  222  333
generator.next(); //全部打印

生成器函数参数和返回值

  • 生成器是特殊的 迭代器,因此在调用next方法的时候,是有返回值的
  • 是通过yield拿到其返回值:yield value
function* foo() {
  console.log(111);
  console.log(222);
  yield "bbb";
  console.log(333);
  yield "ccc";
  console.log(444);
  console.log(555);
  console.log(666);
  console.log(777);
}

const generator = foo();
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
/*
111
222
{ value: 'bbb', done: false }
333
{ value: 'ccc', done: false }
444
555
666
777
{ value: undefined, done: true }
*/
  • 通过上述代码,我们可以看到,next方法的返回值就是之前迭代器next方法的返回值
  • 当函数内部执行完成之后,返回的 done:true
  • 同理,若在函数中途return出去了,则done也为true,value就是return的结果,且后面的代码不会执行
function* foo() {
  console.log(111);
  console.log(222);
  yield "bbb";
  console.log(333);
  return 0;
    
    
  yield "ccc";
  console.log(444);
  console.log(555);
  console.log(666);
  console.log(777);
}

const generator = foo();
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());

/*
111
222
{ value: 'bbb', done: false }
333
{ value: 0, done: true }
{ value: undefined, done: true }
*/
  • 那么我们想在next方法中传入参数,并将这个参数在函数的内部执行,又该怎么操作
    • 一般第一个next方法不会传值
    • 第一个next方法执行的时候,函数内部代码的参数,是由生成器函数传入的
    • 第二个next方法中开始传入参数,其参数是给生成器函数内部第一个yield后面的代码,第三个next方法参数,给第二个yield后面的代码
    • 在函数内部接收参数的时候 const 变量名 = yield 返回值,变量是传递给下面的代码的
function* foo(next1) {
  console.log(111, next1);
  console.log(222, next1);
  const next2 = yield "bbb";
  console.log(333, next2);
  const next3 = yield "ccc";
  console.log(444, next3);
  console.log(555, next3);
  console.log(666, next3);
  console.log(777, next3);
}

const generator = foo("我是第一个参数");
generator.next();
generator.next("我是第二个参数");
generator.next("我是第三个参数");
/*
111 我是第一个参数
222 我是第一个参数
333 我是第二个参数
444 我是第三个参数
555 我是第三个参数
666 我是第三个参数
777 我是第三个参数
*/

生成器中断(用的比较少)

迭代器中由return方法,可以是迭代器中断,因此在生成器中,我们可以直接调用return方法或者throw一个error来终止函数的执行

function* foo(next1) {
  console.log(111, next1);
  console.log(222, next1);
  const next2 = yield "bbb";
  console.log(333, next2);
  const next3 = yield "ccc";
  console.log(444, next3);
  console.log(555, next3);
  console.log(666, next3);
  console.log(777, next3);
}

const generator = foo("我是第一个参数");
generator.next();
generator.return("我是第二个参数"); //后面的函数不会执行,但是next会有返回值  
generator.throw(new Error("我是一个错误"))//与return执行逻辑是一样的,但是需要捕获异常
generator.next("我是第三个参数");

生成器代替迭代器的应用场景

结合以上知识我们可以对之前写过的迭代器进行代替,用更少的代码实现相似的功能

  • 首先 生成器函数执行的结果,会返回一个生成器对象
  • 这个生成器对象,就是特殊的迭代器,可以直接调用next方法
  • 之前使用函数 生成对应数组迭代器的函数就可以简化为以下代码
function* createGeneratorArr(arrName) {
  // index = 0;
  // //返回一个对象,对象中包含next方法,所以可以链式调用
  // return {
  //   next() {
  //     if (index < arrName.length) {
  //       return { done: false, value: arrName[index++] };
  //     } else {
  //       //遍历完成只会,done为false,value可以省略
  //       return { done: true };
  //     }
  //   },
  // };
  /*
  先前的做法是通过这个函数,返回一个所谓迭代器对象,通过这个对象调用next方法
  我们借助生成器函数返回生成器对象的原理
  */

  for (let i = 0; i < arrName.length; i++) {
    yield arrName[i];
  }
}

let arr = [1, 2, 3, 4, 5, 6];
let arrGenerator = createGeneratorArr(arr);
console.log(arrGenerator.next());
console.log(arrGenerator.next());
console.log(arrGenerator.next());
/**{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false } */

生成器自定义类的迭代对象

在迭代器的学习中,我们在一个类中,创建了相应的方法去生成可迭代对象,同理,我们可以用生成器对代码进行优化

  • 首先需要了解 yield*的机制
    • yield*实际上是yield的语法糖
    • 后面接的是可迭代对象,会依次迭代对象中的每个元素
function* createGeneratorArr(arrName) {

    // for (let i = 0; i < arrName.length; i++) {
    //  yield arrName[i];
    // }

    /*
		刚刚的代码,我们使用了for循环对其进行yield控制
		我们可以用yield* addName 直接替代该循环
    */
    
    yield* arrName
}
  • 在了解了 yield* 的机制之后,我们就可以将自定义类的代码进行优化
class Person {
  constructor(name, age, friends) {
    this.name = name;
    this.age = age;
    this.friends = friends;
  }
  //在实例对象上创建实例方法
  // [Symbol.iterator]() {
  //   let index = 0;
  //   //对对象的key进行迭代
  //   let keys = Object.keys(this);
  //   const iterator = {
  //     next: () => {
  //       if (index < keys.length) {
  //         return { done: false, value: keys[index++] };
  //       } else {
  //         return { done: true };
  //       }
  //     },
  //   };
  //   return iterator;
  // }

  /*要用生成器函数*/
  *[Symbol.iterator]() {
    //迭代元素的属性
    yield* Object.keys(this);
    //迭代元素的值
    yield* Object.values(this);
    //迭代元素的键值对
    yield* Object.entries(this);
  }
}

let p = new Person("zhangcheng", 18, "jame");

for (const iterator of p) {
  console.log(iterator);
}

你可能感兴趣的:(重学JavaScript高级,javascript,开发语言,ecmascript)