JavaScript生成器与迭代器
Published on Mar 04, 2023, with 12 view(s) and 0 comment(s)
Ai 摘要:本文介绍了JavaScript中的迭代器与生成器。迭代器是帮助遍历数据结构的对象,遵循迭代器协议,通过next()方法返回{done, value}对象。可迭代对象需实现Symbol.iterator方法,支持for...of等操作。生成器是特殊迭代器,通过function*和yield控制执行流程,能暂停/恢复函数执行。文章详细讲解了迭代器原理、可迭代对象应用、自定义类迭代实现,以及生成器的参数传递、异常处理和替代迭代器等高级用法,展示了ES6这两个特性如何提升迭代操作的清晰度和灵活性。(149字)

迭代的英文“iteration”源自拉丁文itero,意思是“重复”或“再来”。在软件开发领域,“迭代”的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。 ECMAScript 6规范新增了两个高级特性:迭代器和生成器。使用这两个特性,能够更清晰、高效、方便地实现迭代。

1、理解迭代

在JavaScript中,计数循环就是一种最简单的迭代

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

迭代会在一个有序集合上进行,(“有序”可以理解为集合中所有项都可以按照既定的顺序被遍历到,特别是开始和结束项有明确的定义。)数组是JavaScript中有序集合的最典型例子。

let collection = ["foo", "bar", "baz"];
for (let index = 0; index < collection.length; ++index) {
  console.log(collection[index]);
}

因为数组有已知的长度,且数组每一项都可以通过索引获取,所以整个数组可以通过递增索引来遍历。由于如下原因,通过这种循环来执行例程并不理想:

1、迭代之前需要事先知道如何使用数据结构。数组中的每一项都只能先通过引用取得数组对象,然后再通过[]操作符取得特定索引位置上的项。这种情况并不适用于所有数据结构。 2、遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构。

ES5新增了Array.prototype.forEach()方法,向通用迭代需求迈进了一步,这个方法解决了单独记录索引和通过数组对象取得值的问题。因此这个方法只适用于数组,而且回调结构也比较笨拙。

2、迭代器

迭代器(iterator),是确使用户可在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。

  • 其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;

  • 在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;

从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象

在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;在js中这个标准就是一个特定的next方法;

next方法有如下的要求: 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:

done(boolean)

  • 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。

value

  • 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

迭代器代码练习:

let names = ["sun", "james", "lebron"];

let index = 0;

let iterator = {
  next: function () {
    if (index < names.length) {
      return {
        done: false,
        value: names[index++],
      };
    } else {
      return {
        done: true,
        value: undefined,
      };
    }
  },
};
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// { done: false, value: "sun" }
// { done: false, value: "james" }
// { done: false, value: "lebron" }
// { done: true, value: undefined }
// { done: true, value: undefined }
let names = ["sun", "james", "lebron"];

function createArrayIterator(arr) {
  let index = 0;
  return {
    next: function () {
      if (index < arr.length) {
        return { done: false, value: arr[index++] };
      } else {
        return { done: true, value: undefined };
      }
    },
  };
}
const iterator = createArrayIterator(names);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

可迭代对象但是上面的代码整体来说看起来是有点奇怪的:

  • 迭代一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;

  • 事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;

当一个对象实现了可迭代协议时,它就是一个可迭代对象;这个对象的要求是必须实现 @@iterator 方法,在代码中我们使用 Symbol.iterator 访问该属性;

代码演示:

const iterableObj = {
  names: ["sun", "james", "lebron"],
  [Symbol.iterator]: function () {
    let index = 0;
    return {
      next: () => {
        if (index < this.names.length) {
          return { done: false, value: this.names[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  },
};
let iterator = iterableObj[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

转成这样的一个东西有什么好处呢?当一个对象变成一个可迭代对象的时候,进行某些迭代操作,比如 for…of 操作时,其实就会调用它的 @@iterator 方法;

原生迭代器对象平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:String、Array、Map、Set、arguments对象、NodeList集合;

可迭代对象的应用1、JavaScript中语法:for …of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment); 2、创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]); 3、一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);

let names = {
  name1: "sun",
  name2: "james",
  name3: "lebron",
};
console.log(...iterableObj); //sun james lebron
console.log(...names); //TypeError: Found non-callable @@iterator
console.log({ ...names }); 
//{ name1: "sun", name2: "james", name3: "lebron" }  es9实现,用的不是可迭代对象

//创建一些对象
const set = new Set(iterableObj);
console.log(set); //Set(3) { "sun", "james", "lebron" }

自定义类的迭代在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象:如果我们也希望自己的类创建出来的对象默认是可迭代的,那么在设计类的时候我们就可以添加上@@iterator 方法;

class ClassRoom {
  constructor(name, address, students) {
    (this.name = name), (this.address = address), (this.students = students);
  }
  [Symbol.iterator]() {
    let index = 0;

    return {
      next: () => {
        if (index < this.students.length) {
          return { done: false, value: this.students[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  }
}
const r1 = new ClassRoom(301, "8e", ["sun", "james", "lebron"]);
for (let s of r1) {
  console.log(s);
}

迭代器的中断迭代器在某些情况下会在没有完全迭代的情况下中断:比如遍历的过程中通过break、continue、return、throw中断了循环操作;比如在解构的时候,没有解构所有的值;想要监听中断的话,可以添加return方法:

class ClassRoom {
  constructor(name, address, students) {
    (this.name = name), (this.address = address), (this.students = students);
  }
  [Symbol.iterator]() {
    let index = 0;

    return {
      next: () => {
        if (index < this.students.length) {
          return { done: false, value: this.students[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
      return() {
        console.log("监听到迭代停止");
        return {
          done: true,
        };
      },
    };
  }
}
const r1 = new ClassRoom(301, "8e", ["sun", "james", "lebron"]);
for (let s of r1) {
  console.log(s);
  if (s === "james") {
    break;
  }
}

3、生成器

生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。

生成器函数也是一个函数,但是和普通的函数有一些区别: 1、生成器函数需要在function的后面加一个符号:* 2、生成器函数可以通过yield关键字来控制函数的执行流程 3、生成器函数的返回值是一个Generator(生成器),生成器事实上是一种特殊的迭代器

生成器函数执行

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

生成器函数say返回一个生成器对象(这个生成器对象本质上是一个特殊的迭代器对象),通过调用next方法控制函数执行。

next方法是有返回值的:

{ value: undefined, done: false }

如果不希望next方法返回这个对象,可以通过 yield value来实现。

生成器传递参数 – next函数在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值

function* say() {
  console.log(1);
  const n = yield;
  console.log(n);
}
const generator = say();
generator.next();
generator.next(2);
// 1
// 2

生成器提前结束 – return函数还有一个可以给生成器函数传递参数的方法是通过return函数:return传值后这个生成器函数就会结束,之后调用next不会继续生成值了

function* foo() {
  console.log(1);
  const value1 = yield "sun";
  console.log("value1: ", value1);
  const value2 = yield value1;
  //不会执行以下代码
  const value3 = yield value2;
}
const generator = foo();
console.log(generator.next());
console.log(generator.return(123));
console.log(generator.next());

生成器抛出异常 – throw函数除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常:抛出异常后我们可以在生成器函数中捕获异常;但是在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行。

function* foo() {
  console.log(1);
  try {
    yield 1;
  } catch (error) {
    console.log(error);
  }
  console.log(2);
}
const generator = foo();
generator.next();
generator.throw("error:error message");

生成器替代迭代器因为生成器函数调用next方法返回一个{value:string,done:boolean}对象,同样迭代器函数需要返回的也是。生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器,还可以使用yield*来生产一个可迭代对象:候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值;

function* createArrayIterator(arr) {
  let index = 0;
  yield arr[index++];
  yield arr[index++];
  yield arr[index++];
}
// function* createArrayIterator(arr) {
//   for (let item of arr) {
//     yield item;
//   }
// }

// function* createArrayIterator(arr) {
//   yield* arr;
// }