重学JavaScript【迭代器和生成器】

简介: 重学JavaScript 篇的目的是回顾基础,方便学习框架和源码的时候可以快速定位知识点,查漏补缺,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学JavaScript 篇的目的是回顾基础,方便学习框架和源码的时候可以快速定位知识点,查漏补缺,所有文章都同步在 公众号(道道里的前端栈)github 上。


迭代器(Iterator)


一般在JavaScript里,for循环时最简单的迭代,比如:

for (let i = 0; i <= 10; i++) { 
 console.log(i); 
}

循环是迭代机制的基础,这是因为它可以指定迭代的次数和每次迭代的操作。每次循环都会在下一次迭代开始前完成,并且迭代的顺序都是定好的。

迭代会在一个“有序”的集合上进行,有序的意思是有一定的顺序,不一定是自增或者自减,操作最多的就是数组了。最开始的时候,迭代过程中操作数组只能通过下标的方式获取,但是问题是如果不知道下标,就没法获取和操作值,所以后来增加了 Array.prototype.forEach 方法,里面可以直接获取到 项,算是for循环的递进版,但是仍然有问题的是,要想知道有没有遍历到最后一个,还是得通过下标。

上述只是数组,还有很多种类的集合多少都有自己的局限性,所以在ECMAScript6增加了 迭代器模式

迭代器模式

迭代器模式表示可以把一些结构称之为 可迭代对象(Iterable),因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterable操作。

可迭代的数据类型最新典型的就是数组和类数组等集合对象,它们里面包含的元素是有限的,而且都具有无歧义的遍历顺序。

那可迭代对象到底好在哪儿呢?每个迭代器都有一个可迭代对象,迭代器会暴露对应的可迭代对象的API,这样的话迭代器无需了解可迭代对象内部结构,只需要知道如何取得连续的值!

迭代器的作用大致有三个:

  1. 访问数据
  2. 成员可排列
  3. 方便用 for...of 操作

可迭代协议

实现可迭代协议(也就是Iterable接口)要求同时具备两个条件:

  1. 支持迭代
  2. 可创建实现Iterator接口的对象

也就是说,必须暴露出来一个属性作为“默认迭代器”,而且这个属性必须使用 Symbol.iterator作为键,并且它必须引用一个迭代器工厂函数,并且这个迭代器工厂函数返回一个新的迭代器。在工作过程中,并不需要显式的调用这个工厂函数来生成迭代器,因为实现了迭代协议的所有类型会自动兼容,以下是会调用Iterator接口的场景:

  • for...of
let arr = [1, 2, 3, 4, 5];
let iter = arr[Symbol.iterator]();
for(let i of iter){
  console.log(i)
}
//1 2 3 4 5
  • 数组解构
let [a, b] = [1, 2, 3];
  • 扩展操作符...
var str = 'hello';
[...str] //  ['h','e','l','l','o']
  • Array.from()
  • Map和Set
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
  • Promise.all() 和 Promise.race()
  • yield*操作符
let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

它们会在后台调用工厂函数,隐式创建一个迭代器。再简洁一点,凡是部署了Symbol.iterator属性的数据结构,就称为迭代器接口,凡是部署了迭代器接口的都可以用扩展运算符。

Iterator协议

Iterator使用 next() 方法遍历数据,每次成功调用next(),都会返回一个迭代器对象IteratorResult,其中包含了Iterator返回的下一个值。如果不调用next(),则无法知道迭代器的当前位置。

next()方法返回的迭代器对象 IteratorResult 包含2个属性:

  • done:一个布尔值,表示是否还可以再次调用next()取得下一个值
  • value:done为true时,value为undefined,done为false时,value为下一个可迭代对象的值
let arr = ["foo", "bar"];
// 迭代器
let iter = arr[Symbol.iterator]();
iter.next(); // {done: false, value: "foo"}
iter.next(); // {done: false, value: "bar"}
iter.next(); // {done: true, value: undefined}

如果可迭代对象被修改了,那么迭代器也会读取响应的改变:

let arr = ['foo', 'baz']; 
let iter = arr[Symbol.iterator]();
iter.next()); // { done: false, value: 'foo' } 
// 在数组中间插入值
arr.splice(1, 0, 'bar'); 
iter.next(); // { done: false, value: 'bar' } 
iter.next(); // { done: false, value: 'baz' } 
iter.next(); // { done: true, value: undefined }

终止迭代

如果想在迭代期间终止,可以通过 return() 方法中断退出。

return必须返回一个有效的IteratorResult对象,一般来说返回 {done: true},当然也可以有其他比较复杂的操作。

如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代:

let arr = [1, 2, 3, 4, 5];
let iter = arr[Symbol.iterator]();
for (let i of iter){
  console.log(i);
  if(i > 2){
    break;
  }
}
 // 1 2 3
for(let i of iter){
  console.log(i)
}
// 4 5

使用

如果想遍历一个类数组,就必须往类数组上加一个Iterator接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 可以执行了
let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}


生成器(Generator)


生成器是一个特别灵活的结构,它封装了多个内部状态,执行一个生成器会返回一个迭代器对象,所以生成器是一个包括了内部状态,返回迭代器对象的函数,也是就是说他可以自定义迭代器!

写法

生成器的形式是一个函数,函数名前加一个星号(*),表示它是一个生成器。哪里可以定义函数,哪里就可以定义生成器。

function* fn(){};
let fn = function* (){};
let obj = {
  * fn()
}
function * fn(){}
//等价于
function* fn(){}
//等价于
function *fn(){}

注意:箭头函数不能用户来定义生成器,星号不受两侧空格的影响

当一个生成器函数被调用,就会产生一个生成器对象,生成器对象默认处于 暂停执行(suspended) 状态,生成器对象也带有Iterator接口,所以拥有next(),调用next后开始执行逻辑:

function* fn(){};
const gen = fn();
gen; // generatorFn (<suspended>)
gen.next; // f next(){ [native code] }
gen.next(); // { done: true, value: undefined } 

生成器只会在第一次调用next方法后开始执行:

function *fn(){
  console.log(123)
};
let gen = *fn();
gen.next(); // 123

yield 可以让生成器停止和开始执行。生成器函数遇到yield之前会正常执行,遇到之后开始暂停,函数作用于的状态会保留。要想继续执行,必须在生成器对象上调用next方法来恢复执行:

function *fn(){
  yield;
  yield "a";
  return "b"
};
let gen = fn();
gen.next(); // {done: false, value: "a"}
gen.next(); // {done: true, value: "b"}

从上面可以看出来,yield生成的值会放在value里,yield退出的生成器函数的状态是false,return退出的生成器函数的状态是true。

所以:生成器函数被调用后,并不执行,返回值是一个指向内部状态的指针对象,也就是Iterator对象,必须调用next方法,将指针移向下一个状态。

yield

前面提到yield是管理生成器停止和开始的,这里把上面的例子详细说一下:

function *fn(){
  yield;
  yield "a";
  return "b"
};
let gen = fn
gen.next(); // {done: false, value: undefined}
gen.next(); // {done: false, value: "a"}
gen.next(); // {done: true, value: "b"}
gen.next(); // {done: true, value: "undefined"}
  1. 调用第一次next方法,生成器方法开始执行,遇到yield表达式,暂停执行后面的操作,将紧跟yield的值作为value返回,没有值的话返回undefined,后面有其他值所以done是false
  2. 调用第二次next方法,继续执行,value是a,如果再次调用next还会输出其他值,所以done是false
  3. 调用第三次next方法,继续执行,value是b,如果再次调用next不会输出其他值了,所以done是true
  4. 调用第四次next方法,继续执行,value没有,所以是undefined,done是true

每次遇到field,函数都会暂停执行,下一次再从该位置出发。一个函数内部只能执行一次return,但是可以执行多次field表达式。正常函数只能返回一个值,生成器函数能返回多个值。

注意:yield只能在用在生成器函数中,用在其他地方会报错:

//1. 
function* fn(){
  function a(){
    yield;
  }
}
//2. 
function* invalidGeneratorFnB() { 
  const b = () => { 
    yield; 
  } 
}

另外,如果yield被用在另一个表达式中,必须放在圆括号内:

function* demo(){
  console.log((yield));
  console.log((yield 123));
}

next方法是可以传递参数的,看下面这个例子:

function * fn(init){
  console.log(init);
  console.log(yield);
  console.log(yield);
}
let gen = fn("a");
gen.next("b"); // a
gen.next("c"); // c
gen.next("d"); // d

传入的b没有输出!

这是因为next有一个规则:第一次调用next()传入的值不会被调用,因为第一次调用是为了开始执行生成器函数的。

再看一个:

function* fn(){
  return yield "a"
}
let gen = fn();
gen.next(); // {value: "a", done: false}
gen.next("b"); // {value: "b", done: true}

因为函数必须对整个表达式求值才可以确定要返回的值,所以在它遇到yield时暂停执行并且计算要产生的值a,然后下一次调用next方法传入了b,然后该值被确定为最终要返回的值。

迭代

因为生成器函数本身就是迭代器生成函数,所以可以直接把生成器赋值给对象的Symbol.iterator属性:

let obj = {
  [Symbol.iterator] = function* (){
    yield 1;
    yield 2;
    yield 3;
  }
}
[...obj]  // [1, 2, 3]

生成器返回的迭代器对象和迭代器对象的Symbol.iterator是相等的:

function *fn(){}
//遍历器对象gen
let gen = fn();
gen === gen[Symbol.iterator]() // true

for...of

使用for...of会自动遍历生成器内部的迭代器对象,而且不再需要next

function* fn(){
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  return 5;
}
for(let value of fn()){
  console.log(value)
}
//1 2 3 4

一旦nex返回的done是true,for...of就会终止,所以没有返回5。

除此之外,扩展运算符,解构赋值和Array.from()调用的也都是迭代器接口:

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


终止生成


终止生成器的方法有 throw()return()

throw

throw方法会在暂停的时候将一个错误注入到生成器对象中,如果错误没有被处理,生成器就会关闭,如果错误在内部处理了,生成器就不会关闭,继续恢复执行。

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

第一个错误被生成器函数体内的catch捕获,输出内容,由于catch已经捕获过了,不会再捕捉到这个错误了,所以错误被抛出了生成器内,第二个错误属于函数题外的了。

如果生成器内部和外部都没有使用try...catch,那么程序会报错:

function* fn(){
  yield console.log('hello');
  yield console.log('world');
}
var g = fn();
g.next();
g.throw();
// hello
// Uncaught undefined

如果catch抛出的错误要被内部捕获,前提是至少执行一次next方法:

function* fn(){
 try {
    yield 1;
  } catch (e) {
    console.log('内部捕获');
  }
var g = fn();
g.throw();
// Uncaught 1

那抛出错误之后的yield还会执行么?

function *fn(){
  for(let x of [1, 2, 3]){
    try {
      yield x;
    } catch(e) {}
    yield console.log("P")
  }
}
const gen = fn();
console.log(gen.next()); // { done: false, value: 1 }
gen.throw("a"); // P
console.log(gen.next()); // { done: false, vlaue: 3}

生成器在try...catch中的yield暂停了,期间throw注入一个错误a,这个错误会被yield抛出。因为错误是在生成器内部的try...catch中抛出的,所以仍然在生成器内部被捕获,接着抛出这个错误替代了要输出的的2,接下来继续执行,再次遇到yield之后输出3。在抛出错误的时候,同样不影响后面的yield进行。

return

return就相当于返回了 {done: true, value: undefined},如果return方法有值,返回的value就是那个值。

function* fn(){
  yield 1;
  yield 2;
}
let gen = fn();
gen.next(); // { value: 1, done: false }
gen.return("a"); // { value: "a", done: true}
gen.next(); // { value: undefined, done: true }

如果生成器里有try...finally,且正在执行try,那么return会导致直接进入finally,然后继续执行:

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


其他


yield*

如果一个生成器内部调用了另一个生成器,需要手动把内部的遍历,yields* 就是为了方便这一操作的:

function* foo() {
  yield 'a';
  yield 'b';
}
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"

yield*后面的生成器函数(没有return时),等同于生成器函数内部部署一个for...of循环:

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;
  }
}

如果有return,就需要用一个变量来存储返回值。

因为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

总结

其实生成器函数依赖迭代器,内部通过yield处理过程,比较像异步操作,每一步next(),都可以去控制,这样就和Promise的功能很像了。同时有迭代器接口的数据类型都可以使用for...of、扩展运算符、解构赋值和Array.from等操作,方便了对于数据的操作,迭代器和生成器的功能还是很强大的!

目录
相关文章
|
6月前
|
前端开发 JavaScript 中间件
掌握JavaScript中的迭代器和生成器(下)
掌握JavaScript中的迭代器和生成器(下)
|
6月前
|
存储 JavaScript 前端开发
掌握JavaScript中的迭代器和生成器(上)
掌握JavaScript中的迭代器和生成器
|
5月前
|
存储 JavaScript 前端开发
javascript中的生成器和迭代器是什么
JavaScript中的生成器和迭代器是处理集合数据的利器,它们提供了一种遍历和操作元素的统一方式。迭代器是具有`next()`方法的对象,返回包含`value`和`done`属性的对象,用于循环处理集合。生成器函数更进一步,可以在执行过程中暂停并返回值,通过`yield`产生迭代值,适用于生成序列、异步编程和实现状态机等场景。例如,一个生成器可以无限生成斐波那契数列,或者在读取文件时控制异步流程。使用这些工具,代码变得更简洁、高效。
|
2月前
|
JavaScript 前端开发 Python
JavaScript写个.ts视频文件Url生成器,使用了string.padStart
JavaScript写个.ts视频文件Url生成器,使用了string.padStart
|
2月前
|
JavaScript 索引
|
4月前
|
存储 JavaScript 前端开发
JavaScript编码之路【ES6新特性之 Symbol 、Set 、Map、迭代器、生成器】(二)
JavaScript编码之路【ES6新特性之 Symbol 、Set 、Map、迭代器、生成器】(二)
54 1
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的计算机网络课程试卷生成器附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的计算机网络课程试卷生成器附带文章和源代码部署视频讲解等
35 2
|
4月前
|
JavaScript 索引
JS的迭代器是啥?精读JS迭代器
JS的迭代器是啥?精读JS迭代器
29 0
|
4月前
|
存储 JavaScript 前端开发
JavaScript编码之路【ES6新特性之 Symbol 、Set 、Map、迭代器、生成器】(一)
JavaScript编码之路【ES6新特性之 Symbol 、Set 、Map、迭代器、生成器】(一)
40 0
|
6月前
|
存储 JavaScript 前端开发
JavaScript中的复杂功能实现:一个动态表单生成器
JavaScript中的复杂功能实现:一个动态表单生成器