JavaScript进阶之(七) 迭代器与生成器

学习整理地址 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Iterators_and_Generators

处理集合中的每个项是很常见的操作。JavaScript 提供了许多迭代集合的方法,从简单的 for 循环到 map() 和 filter()。迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义 for…of 循环的行为。

一、迭代器

在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。 更具体地说,迭代器是通过使用 next() 方法实现 Iterator protocol 的任何一个对象,该方法返回具有两个属性的对象: value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true 。如果 value 和 done 一起存在,则它是迭代器的返回值。

一旦创建,迭代器对象可以通过重复调用next()显式地迭代。 迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次。 在产生终止值之后,对next()的额外调用应该继续返回{done:true}。

Javascript中最常见的迭代器是Array迭代器,它只是按顺序返回关联数组中的每个值。 虽然很容易想象所有迭代器都可以表示为数组,但事实并非如此。 数组必须完整分配,但迭代器仅在必要时使用,因此可以表示无限大小的序列,例如0和无穷大之间的整数范围。

这是一个可以做到这一点的例子。 它允许创建一个简单的范围迭代器,它定义了从开始(包括)到结束(独占)间隔步长的整数序列。 它的最终返回值是它创建的序列的大小,由变量iterationCount跟踪。

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
    let nextIndex = start;
    let iterationCount = 0;

    const rangeIterator = {
       next: function() {
           let result;
           if (nextIndex < end) {
               result = { value: nextIndex, done: false } //返回 值 ,还可以迭代
               nextIndex += step;
               iterationCount++;
               return result;
           }
           return { value: iterationCount, done: true } //结束
       }
    };
    return rangeIterator;
}

使用这个迭代器看起来像这样:

let it = makeRangeIterator(1, 10, 2);

let result = it.next();
while (!result.done) {
 console.log(result.value); // 1 3 5 7 9
 result = it.next();
}

console.log("Iterated over sequence of size: ", result.value); // 5

二、生成器函数

2.1 关于yield

yield关键字使生成器函数执行暂停,yield关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本的return关键字。

yield关键字实际返回一个IteratorResult对象,它有两个属性,value和done。value属性是对yield表达式求值的结果,而done是false,表示生成器函数尚未完全完成。

一旦遇到 yield 表达式,生成器的代码将被暂停运行,直到生成器的 next() 方法被调用。每次调用生成器的next()方法时,生成器都会恢复执行,直到达到以下某个值:

  • yield,导致生成器再次暂停并返回生成器的新值。 下一次调用next()时,在yield之后紧接着的语句继续执行。
  • throw用于从生成器中抛出异常。这让生成器完全停止执行,并在调用者中继续执行,正如通常情况下抛出异常一样。
  • 到达生成器函数的结尾;在这种情况下,生成器的执行结束,并且IteratorResult给调用者返回undefined并且done为true。
  • 到达return
    语句。在这种情况下,生成器的执行结束,并将IteratorResult返回给调用者,其值是由return语句指定的,并且done 为true。

如果将参数传递给生成器的next()方法则该值将成为生成器当前yield操作返回的值。下高级用法使用到传递参数。

在生成器的代码路径中的yield运算符,以及通过将其传递给Generator.prototype.next()指定新的起始值的能力之间,生成器提供了强大的控制力。

2.2 基本用法

虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态
生成器函数使用 ==function*==语法编写。 最初调用时,生成器函数不执行任何代码,而是返回一种称为Generator的迭代器。 通过调用生成器的下一个方法消耗值时,Generator函数将执行,直到遇到yield关键字。

可以根据需要多次调用该函数,并且每次都返回一个新的Generator,但每个Generator只能迭代一次。

我们现在可以调整上面的例子了。 此代码的行为是相同的,但实现更容易编写和读取

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    for (let i = start; i < end; i += step) {
        yield i; // 返回i
    }
}
var a = makeRangeIterator(1,10,2)
a.next() // {value: 1, done: false}
a.next() // {value: 3, done: false}
a.next() // {value: 5, done: false}
a.next() // {value: 7, done: false}
a.next() // {value: 9, done: false}
a.next() // {value: undefined, done: true}

一个无限迭代器

function* idMaker(){
    let index = 0;
    while(true)
        yield index++;  //返回 
}

let gen = idMaker(); // "Generator { }"

console.log(gen.next().value); 
// 0
console.log(gen.next().value); 
// 1
console.log(gen.next().value); 
// 2
// ...

2.3 高级生成器

生成器会按需计算它们的产生值,这使得它们能够有效的表示一个计算成本很高的序列,甚至是如上所示的一个无限序列。

The next() 方法也接受一个参数用于修改生成器内部状态。传递给 next() 的参数值会被yield接收。要注意的是,传给第一个 next() 的值会被忽略。

下面的是斐波那契数列生成器,它使用了 next(x) 来重新启动序列:

function* fibonacci() {
  var fn1 = 0;
  var fn2 = 1;
  while (true) {  
    var current = fn1;
    fn1 = fn2;
    fn2 = current + fn1;
    var reset = yield current;  //参数值将成为生成器当前yield操作返回的值
    if (reset) {
        fn1 = 0;
        fn2 = 1;
    }
  }
}

var sequence = fibonacci();
console.log(sequence.next().value);     // 0
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2
console.log(sequence.next().value);     // 3
console.log(sequence.next().value);     // 5
console.log(sequence.next().value);     // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2

你可以通过调用其 throw() 方法强制生成器抛出异常,并传递应该抛出的异常值。这个异常将从当前挂起的生成器的上下文中抛出,就好像当前挂起的 yield 是一个 throw value 语句。

如果在抛出的异常处理期间没有遇到 yield,则异常将通过调用 throw() 向上传播,对 next() 的后续调用将导致 done 属性为 true。

生成器具有 return(value) 方法,返回给定的值并完成生成器本身。

2.4 关于yield*

yield* 表达式用于委托给另一个generator 或可迭代对象。
委托给其他生成器
以下代码中,g1() yield 出去的每个值都会在 g2() 的 next() 方法中返回,就像那些 yield 语句是写在 g2() 里一样。

function* g1() {
  yield 2;
  yield 3;
  yield 4;
}

function* g2() {
  yield 1;
  yield* g1();
  yield 5;
}

var iterator = g2();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

委托给其他可迭代对象
除了生成器对象这一种可迭代对象,yield* 还可以 yield 其它任意的可迭代对象,比如说数组、字符串、arguments 对象等等。

 function* g3() {
  yield* [1, 2];
  yield* "34";
  yield* arguments;
}

var iterator = g3(5, 6);

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: "3", done: false }
console.log(iterator.next()); // { value: "4", done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: 6, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

三、for…of

for…of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

3.1 迭代Array

let iterable = [10, 20, 30];

for (let value of iterable) {
    value += 1;
    console.log(value);
}
// 11
// 21
// 31

如果你不想修改语句块中的变量 , 也可以使用const代替let。

let iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value);
}
// 10
// 20
// 30

3.2 迭代String

let iterable = "boo";

for (let value of iterable) {
  console.log(value);
}
// "b"
// "o"
// "o"

3.3 迭代 TypedArray

let iterable = new Uint8Array([0x00, 0xff]);

for (let value of iterable) {
  console.log(value);
}
// 0
// 255

3.4 迭代 Set / Map

let iterable = new Set([1, 1, 2, 2, 3, 3]);

for (let value of iterable) {
  console.log(value);
}
// 1
// 2
// 3
let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);

for (let entry of iterable) {
  console.log(entry);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]

for (let [key, value] of iterable) {
  console.log(value);
}
// 1
// 2
// 3

3.5 迭代 arguments 对象

(function() {
  for (let argument of arguments) {
    console.log(argument);
  }
})(1, 2, 3);

// 1
// 2
// 3

3.6 迭代 DOM 集合

迭代 DOM 元素集合,比如一个NodeList对象:下面的例子演示给每一个 article 标签内的 p 标签添加一个 “read” 类。

//注意:这只能在实现了NodeList.prototype[Symbol.iterator]的平台上运行
let articleParagraphs = document.querySelectorAll("article > p");

for (let paragraph of articleParagraphs) {
  paragraph.classList.add("read");
}

3.7 关闭迭代器

对于for…of的循环,可以由break, throw continue 或return终止。在这些情况下,迭代器关闭。

function* foo(){ 
  yield 1; 
  yield 2; 
  yield 3; 
}; 

for (let o of foo()) { 
  console.log(o); 
  break; // closes iterator, triggers return
}

3.8 迭代生成器

你还可以迭代一个生成器:

function* fibonacci() { // 一个生成器函数
    let [prev, curr] = [0, 1];
    for (;;) { // while (true) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}
 
for (let n of fibonacci()) {
     console.log(n); 
    // 当n大于1000时跳出循环
    if (n >= 1000)
        break;
}

生成器不应该重用,即使for…of循环的提前终止,例如通过break关键字。在退出循环后,生成器关闭,并尝试再次迭代,不会产生任何进一步的结果。

var gen = (function *(){
    yield 1;
    yield 2;
    yield 3;
})();
for (let o of gen) {
    console.log(o);
    break;//关闭生成器
} 

//生成器不应该重用,以下没有意义!
for (let o of gen) {
    console.log(o);
}

3.9 迭代其他可迭代对象

var iterable = {
  [Symbol.iterator]() {
    return {
      i: 0,
      next() {
        if (this.i < 3) {
          return { value: this.i++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

for (var value of iterable) {
  console.log(value);
}
// 0
// 1
// 2

3.10 for…of与for…in的区别

无论是for…in还是for…of语句都是迭代一些东西。它们之间的主要区别在于它们的迭代方式。

  • for…in 语句以任意顺序迭代对象的可枚举属性
  • for…of 语句遍历可迭代对象定义要迭代的数据

以下示例显示了与Array一起使用时,for…of循环和for…in循环之间的区别。

Object.prototype.objCustom = function() {}; 
Array.prototype.arrCustom = function() {};

let iterable = [3, 5, 7];
iterable.foo = 'hello';

for (let i in iterable) {
  console.log(i); // logs 0, 1, 2, "foo", "arrCustom", "objCustom" // 以任意顺序迭代对象的可枚举属性
}

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i); // logs 0, 1, 2, "foo"
  }
}

for (let i of iterable) {
  console.log(i); // logs 3, 5, 7  //遍历可迭代对象定义要迭代的数据
}
 

例子分步分析:

Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {}; 

const iterable = [3, 5, 7]; 
iterable.foo = 'hello';

每个对象将继承objCustom属性,并且作为Array的每个对象将继承arrCustom属性,因为将这些属性添加到Object.prototype和Array.prototype。由于继承和原型链,对象iterable继承属性objCustom和arrCustom。
在这里插入图片描述

for (let i in iterable) {
  console.log(i); // logs 0, 1, 2, "foo", "arrCustom", "objCustom" 
}

此循环仅以原始插入顺序记录iterable 对象的可枚举属性。它不记录数组元素3, 5, 7 或hello,因为这些不是枚举属性,见图片。但是它记录了数组索引以及arrCustom和objCustom。

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i); // logs 0, 1, 2, "foo"
  }
}

这个循环类似于第一个,但是它使用hasOwnProperty() 来检查,如果找到的枚举属性是对象自己的(不是继承的)。如果是,该属性被记录。记录的属性是0, 1, 2和foo,因为它们是自身的属性(不是继承的)。属性arrCustom和objCustom不会被记录,因为它们是继承的。

for (let i of iterable) {
  console.log(i); // logs 3, 5, 7 
}

该循环迭代并记录iterable作为可迭代对象定义的迭代值,这些是数组元素 3, 5, 7,而不是任何对象的属性。

四、 提高之迭代协议

3.1 迭代协议

作为 ECMAScript 2015 的一组补充规范,迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。
迭代协议具体分为两个协议:可迭代协议和迭代器协议。

3.1.1可迭代协议

可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for…of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object))。

要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性

属性
[Symbol.iterator]一个无参数的函数,其返回值为一个符合迭代器协议的对象。

当一个对象需要被迭代的时候(比如被置入一个 for…of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。

值得注意的是调用此零个参数函数时,它将作为对可迭代对象的方法进行调用。 因此,在函数内部,this关键字可用于访问可迭代对象的属性,以决定在迭代过程中提供什么。

此函数可以是普通函数,也可以是生成器函数,以便在调用时返回迭代器对象。 在此生成器函数的内部,可以使用yield提供每个条目。

3.1.2 迭代器协议

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。

只有实现了一个拥有以下语义(semantic)的== next() 方法==,一个对象才能成为迭代器:
一个无参数函数,返回一个应当拥有以下两个属性的对象:

  • done(boolean)如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
  • value迭代器返回的任何 JavaScript 值。done 为 true 时可省略。next()方法必须返回一个对象,该对象应当有两个属性: done 和 value,如果返回了一个非对象值(比如 false 或undefined),则会抛出一个 TypeError 异常(“iterator.next() returned a non-object value”)。

不可能判断一个特定的对象是否实现了迭代器协议,然而,创造一个同时满足迭代器协议和可迭代协议的对象是很容易的(如下面的示例中所示)。

这样做允许一个迭代器能被各种需要可迭代对象的语法所使用。因此,很少会只实现迭代器协议,而不实现可迭代协议。

var myIterator = {
    next: function() {
        // ...
    },
    [Symbol.iterator]: function() { return this }
}

3.2 使用迭代协议的例子

String 是一个内置的可迭代对象:

let someString = "hi";
typeof someString[Symbol.iterator];          // "function"

String 的默认迭代器会依次返回该字符串的各码点(code point):

let iterator = someString[Symbol.iterator]();
iterator + "";                               // "[object String Iterator]"
 
iterator.next();                             // { value: "h", done: false }
iterator.next();                             // { value: "i", done: false }
iterator.next();                             // { value: undefined, done: true }

我们可以通过提供自己的 @@iterator 方法,重新定义迭代行为:

// 必须构造 String 对象以避免字符串字面量 auto-boxing
var someString = new String("hi");
someString[Symbol.iterator] = function() {
  return { // 只返回一次元素,字符串 "bye",的迭代器对象
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

注意重新定义的 @@iterator 方法是如何影响内置语法结构的行为的:

[...someString];                              // ["bye"]
someString + "";                              // "hi"

3.3 可迭代对象示例

3.3.1 内置可迭代对象

目前所有的内置可迭代对象如下:String、Array、TypedArray、Map 和 Set,它们的原型对象都实现了 @@iterator 方法。

3.3.2自定义可迭代对象

我们可以实现一个自己的可迭代对象,就像这样:

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1; //它可以被认为是一个基于生成器的版本的return关键字
    yield 2;
    yield 3;
};
[...myIterable]; // [1, 2, 3]

3.3.3接受可迭代对象的内置 API

很多 API 接受可迭代对象作为参数,例如:

  • new Map([iterable])
  • new WeakMap([iterable])
  • new Set([iterable])
  • new WeakSet([iterable])
  • Promise.all(iterable)
  • Promise.race(iterable)
  • Array.from(iterable)
new Map([[1, 'a'], [2, 'b'], [3, 'c']]).get(2); // "b"

let myObj = {};

new WeakMap([
    [{}, 'a'],
    [myObj, 'b'],
    [{}, 'c']
]).get(myObj);             // "b"

new Set([1, 2, 3]).has(3); // true
new Set('123').has('2');   // true

new WeakSet(function* () {
    yield {} 
    yield myObj 
    yield {} 
}()).has(myObj);           // true

3.3.4需要可迭代对象的语法

一些语句和表达式需要可迭代对象,比如 for…of 循环、展开语法、yield*,和结构赋值。

for(let value of ["a", "b", "c"]){
    console.log(value);
}
// "a"
// "b"
// "c"

[..."abc"]; // ["a", "b", "c"]

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

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

[a, b, c] = new Set(["a", "b", "c"]);
a // "a"

3.4 迭代器示例

3.4.1 简单迭代器

function makeIterator(array) {
    let nextIndex = 0;
    return {
       next: function () {
           return nextIndex < array.length ? {
               value: array[nextIndex++],
               done: false
           } : {
               done: true
           };
       }
    };
}

let it = makeIterator(['哟', '呀']);

console.log(it.next().value); // '哟'
console.log(it.next().value); // '呀'
console.log(it.next().done);  // true

3.4.2 无穷迭代器

function idMaker() {
    let index = 0;
    return {
       next: function() {
           return {
               value: index++,
               done: false
           };
       }
    };
}

let it = idMaker();

console.log(it.next().value); // '0'
console.log(it.next().value); // '1'
console.log(it.next().value); // '2'
// ...

3.4.3 使用生成器

function* makeSimpleGenerator(array) {
    let nextIndex = 0;
    
    while(nextIndex < array.length) {
        yield array[nextIndex++];
    }
}

let gen = makeSimpleGenerator(['哟', '呀']);

console.log(gen.next().value); // '哟'
console.log(gen.next().value); // '呀'
console.log(gen.next().done);  // true



function* idMaker() {
    let index = 0;
    while (true) {
        yield index++;
    }
}

let gen = idMaker();

console.log(gen.next().value); // '0'
console.log(gen.next().value); // '1'
console.log(gen.next().value); // '2'
// ...

3.4.4 ES2015 类 class 中的迭代器

class SimpleClass {
  constructor(data) {
    this.data = data
  }

  [Symbol.iterator]() {
    // Use a new index for each iterator. This makes multiple
    // iterations over the iterable safe for non-trivial cases,
    // such as use of break or nested looping over the same iterable.
    let index = 0;

    return {
      next: () => {
        if (index < this.data.length) {
          return {value: this.data[index++], done: false}
        } else {
          return {done: true}  
        }
      }
    }
  }
}

const simple = new SimpleClass([1,2,3,4,5])

for (const val of simple) {
  console.log(val)   //'1' '2' '3' '4' '5' 
}

3.5 生成器对象到底是一个迭代器,还是一个可迭代对象?

生成器对象既是迭代器,也是可迭代对象:

let aGeneratorObject = function* (){
    yield 1;
    yield 2;
    yield 3;
}();

typeof aGeneratorObject.next;
// 返回"function", 因为有一个next方法,所以这是一个迭代器

typeof aGeneratorObject[Symbol.iterator];
// 返回"function", 因为有一个@@iterator方法,所以这是一个可迭代对象

aGeneratorObject[Symbol.iterator]() === aGeneratorObject;
// 返回true, 因为@@iterator方法返回自身(即迭代器),所以这是一个格式良好的可迭代对象

[...aGeneratorObject];
// 返回[1, 2, 3]

console.log(Symbol.iterator in aGeneratorObject)
// 返回true, 因为@@iterator方法是aGeneratorObject的一个属性
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值