跳至主要內容

迭代器和生成器

樱桃茶大约 10 分钟

迭代器和生成器

迭代器(Iterators)和生成器(Generators)是 TypeScript 中用来处理集合数据的特性,它们遵循 ES6 规范。

  • 迭代器(Iterators):

    • 迭代器是一个对象,它定义了一个序列,并且可以按需返回序列中的下一个值。
    • 它必须实现一个 next() 方法,该方法返回一个包含两个属性的对象:value(下一个值)和 done(布尔值,表示是否有更多的值可供迭代)。
    • 当没有更多值时,done 属性为 true;此时 value 可能不会被设置。
    • TypeScript 支持通过 for...of 循环来遍历实现了迭代器协议的对象。
    let someArray = [1, 2, 3];
    for (let value of someArray) {
      console.log(value); // 输出: 1, 2, 3
    }
    
  • 生成器(Generators):

    • 生成器是一种特殊类型的函数,可以使用 function* 声明,它可以使用 yield 关键字暂停和恢复其执行状态。
    • 当生成器函数被调用时,它并不立即执行,而是返回一个迭代器,通过这个迭代器可以控制函数的执行。
    • 每次调用迭代器的 next() 方法时,生成器函数会执行到下一个 yield 表达式,并返回一个 { value: Any, done: Boolean } 对象,其中 valueyield 表达式的结果,done 指示生成器是否已经产出了它的最后一个值。
    function* generator() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    let iterator = generator();
    
    console.log(iterator.next().value); // 输出: 1
    console.log(iterator.next().value); // 输出: 2
    console.log(iterator.next().value); // 输出: 3
    console.log(iterator.next().done); // 输出: true
    

迭代器和生成器提供了强大的方式来处理数据集合,尤其对于需要进行懒序列处理或复杂逻辑处理的场景。在 TypeScript 中,你可以利用它们来编写更加高效和易于管理的异步代码。

可迭代性

可迭代性(Iterability)在 TypeScript 中指的是对象的能力,意味着它们可以定义一个迭代的过程。这种能力允许你使用比如for...of循环遍历对象中的值。在 JavaScript 中,数组就是最常见的可迭代对象。

要使类型支持迭代,在 TypeScript 中你需要使用到Symbol.iterator属性,这是一个返回迭代器的函数。迭代器是一个具有next()方法的对象,每次调用next()都会返回一个形如{done: boolean, value: any}的对象。当donetrue时表示迭代结束了。

以下是一些关于可迭代性的重点:

  • 实现可迭代协议:为了让自定义对象可迭代,你必须在对象上实现[Symbol.iterator]()方法。

  • Array 和 Set:在 JavaScript 和 TypeScript 中,数组(Array)和集合(Set)都已经内置了迭代器,所以可以直接在for...of循环中使用。

  • 字符串也是可迭代的:字符串是字符的一个序列,你可以使用for...of循环来遍历一个字符串中的所有字符。

以下是一些例子:

// 数组的可迭代性
let numbers = [1, 2, 3];
for (let num of numbers) {
  console.log(num); // 输出 1, 2, 3
}

// 字符串的可迭代性
let greeting = "hello";
for (let char of greeting) {
  console.log(char); // 输出 'h', 'e', 'l', 'l', 'o'
}

// 自定义可迭代对象
class MyCollection {
  private items: number[];

  constructor(items: number[]) {
    this.items = items;
  }

  // 实现 [Symbol.iterator] 方法
  [Symbol.iterator]() {
    let i = 0;
    return {
      next: () => {
        if (i < this.items.length) {
          return { done: false, value: this.items[i++] };
        } else {
          return { done: true };
        }
      },
    };
  }
}

// 使用自定义的可迭代对象
let collection = new MyCollection([100, 200, 300]);
for (let item of collection) {
  console.log(item); // 输出 100, 200, 300
}

注意:虽然for...in循环能够遍历对象的键,但它不是基于迭代器协议的,而且通常不建议用于数组的迭代,因为它并不仅限于元素索引的遍历,还会遍历对象的所有可枚举属性。

for..of 语句

迭代器和生成器 > 可迭代性 > for..of 语句

  • for..of 语句用于遍历可迭代对象的元素。在 JavaScript 中,一些内置类型如数组、字符串、Map 和 Set 都是可迭代的。
  • 使用for..of循环时,它会自动请求下一个值,直到遍历完所有值。
  • TypeScript 中的for..of提供了一种简单且清晰的方法来遍历各种集合。

例子:

let someArray = [1, "string", false];

// 遍历数组
for (let entry of someArray) {
  console.log(entry); // 1, "string", false
}

// 遍历字符串中的字符
for (let char of "hello") {
  console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
  • 当你使用for..of语句来遍历对象时,你会得到对象中每个元素的值。
  • 不同于for..in,它遍历的是对象的键,而for..of遍历的是对象的值。这使得for..of更适合需要操作数据值时的情况。

例子:

let pets = new Set(["Cat", "Dog", "Hamster"]);

// 遍历集合(Set)
for (let pet of pets) {
  console.log(pet); // "Cat", "Dog", "Hamster"
}

let numbers = new Map<number, string>([
  [1, "One"],
  [2, "Two"],
  [3, "Three"],
]);

// 遍历映射(Map)
for (let [num, numString] of numbers) {
  console.log(`${num} stands for ${numString}`);
  // "1 stands for One", "2 stands for Two", "3 stands for Three"
}
  • 如果一个对象没有实现Symbol.iterator属性,则它不是可迭代的,不能使用for..of进行遍历。
  • 为了让 TypeScript 知道一个对象是可迭代的,你可以实现该对象的迭代器接口(Iterable接口)。

例子:

// 自定义迭代器
class ComponentCollection {
  private components: string[];

  constructor() {
    this.components = ["Component1", "Component2", "Component3"];
  }

  [Symbol.iterator]() {
    let pointer = 0;
    let components = this.components;

    return {
      next(): IteratorResult<string> {
        if (pointer < components.length) {
          return {
            done: false,
            value: components[pointer++],
          };
        } else {
          return {
            done: true,
            value: null,
          };
        }
      },
    };
  }
}

let myComponents = new ComponentCollection();

// 使用 for..of 遍历自定义的类实例
for (let comp of myComponents) {
  console.log(comp);
}
  • 在上面的例子中,我们创建了一个名为ComponentCollection的类,并使其成为可迭代的,通过添加一个[Symbol.iterator]()方法。这样就可以使用for..of来遍历myComponents实例中的组件了。

for..of vs. for..in 语句

迭代器和生成器 > 可迭代性 > for..of 语句 > for..of vs. for..in 语句

  • for..of 语句用于遍历可迭代对象(如数组、字符串、Map、Set 等)的元素。

    示例:

    let someArray = [1, "string", false];
    
    for (let entry of someArray) {
      console.log(entry); // 1, 'string', false
    }
    
  • for..in 语句用于遍历一个对象的属性名(也就是对象的键)。

    示例:

    let list = { a: 1, b: 2, c: 3 };
    
    for (let i in list) {
      console.log(i); // 'a', 'b', 'c'
    }
    
  • for..of 提供了一种访问数据元素值的简洁方法,而不需要使用索引。

  • for..in 循环出的是对象的键,适合需要处理对象属性时使用,但如果用在数组上,它可能返回预期之外的键(如数组对象的自定义属性或者原型链上的属性),并且它的顺序可能会变。

    示例:

    let array = [10, 20, 30];
    array.foo = "Hello"; // 给数组添加一个属性
    
    for (let i in array) {
      console.log(i); // 输出 '0', '1', '2', 'foo'
    }
    
    for (let i of array) {
      console.log(i); // 输出 10, 20, 30
    }
    
  • 当要遍历数组和想要获取元素值时,推荐使用 for..of

  • 当要遍历对象属性时,可以使用 for..in,但要注意它还会枚举原型链上的属性。

代码生成

for...of 语句是 TypeScript 和 JavaScript 中用于遍历可迭代对象(如数组、字符串、Map、Set 等)的元素的一种简洁方法。与 for...in 语句不同,它不是用来遍历对象的键,而是用来遍历可迭代对象的值。

在讨论代码生成时,我们指的是 TypeScript 编译器将 for...of 循环转换成 JavaScript 代码的过程。TypeScript 需要根据目标版本的 JavaScript 来决定如何生成代码,因为不是所有版本的 JavaScript 都支持 for...of 循环或迭代器协议。

  • 当目标是 ES5 或更低版本,for...of 会被编译成一个基于 for 循环和额外辅助函数的形式。

    例子:

    let arr = [1, 2, 3];
    for (let value of arr) {
      console.log(value); // 输出: 1, 2, 3
    }
    

    上面的代码会被编译成类似以下的 ES5 代码:

    var arr = [1, 2, 3];
    for (var _i = 0, _a = arr; _i < _a.length; _i++) {
      var value = _a[_i];
      console.log(value);
    }
    
  • 如果目标是 ES6 或更高版本,则 for...of 可以直接在生成的 JavaScript 代码中使用,因为这些版本原生支持 for...of 循环和迭代器。

    使用 ES6 的例子与上述 TypeScript 代码相同,但生成的代码将保持 for...of 形式,因为 ES6 支持它。

了解 for...of 语句如何被编译能够帮助你理解兼容性问题以及 TypeScript 如何处理新特性与旧环境的关系。记住,即使你的 TypeScript 代码中使用了最新的特性,编译器也会帮你转换成目标环境支持的 JavaScript 代码。

目标为 ES5 和 ES3
  • 在 TypeScript 中,代码生成是指 TypeScript 编译器如何将 TypeScript 代码转换为 JavaScript 代码,因为浏览器和其他 JavaScript 环境并不直接执行 TypeScript。

  • 当你使用 TypeScript 写for..of循环时,TypeScript 需要将这个循环转化成一个在目标 JavaScript 环境中可运行的形式。

  • 如果你设定编译器的目标(target)ES5ES3,它不能直接使用 ES6 的for..of语法,因为 ES5 或 ES3 标准中没有原生支持这个特性。

  • TypeScript 会将for..of循环转化为一个旧版本的 JavaScript 循环结构,通常是一个基于迭代器的 while 循环。例如:

    let someArray = [1, "string", false];
    
    for (let item of someArray) {
      console.log(item); // 1, "string", false
    }
    
  • 编译到 ES5 后,上面的代码可能会变成类似这样:

    var someArray = [1, "string", false];
    var _iteratorNormalCompletion = true;
    var _didIteratorError = false;
    var _iteratorError = undefined;
    
    try {
      for (
        var _iterator = someArray[Symbol.iterator](), _step;
        !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
        _iteratorNormalCompletion = true
      ) {
        var item = _step.value;
        console.log(item); // 1, "string", false
      }
    } catch (err) {
      _didIteratorError = true;
      _iteratorError = err;
    } finally {
      try {
        if (!_iteratorNormalCompletion && _iterator.return != null) {
          _iterator.return();
        }
      } finally {
        if (_didIteratorError) {
          throw _iteratorError;
        }
      }
    }
    
  • 注意,在这个例子中,Symbol.iterator和相应的逻辑用来模仿 ES6 的迭代器协议,但如果目标是 ES3,甚至连 Symbol 都无法使用,所以 TypeScript 会采用不同的方法来确保兼容。

  • 这种转换意味着可以在老版 JavaScript 环境中使用 TypeScript 编写的现代代码,但可能会导致生成的代码更长、更难阅读。

  • 对于新手来说,重要的是了解虽然你可能正在使用最新的 TypeScript 或 JavaScript 特性,但编译后的代码需要能在目标环境(如 ES5 浏览器)中运行。这就是编译器设定目标版本的原因。

目标为 ECMAScript 2015 或更高
  • TypeScript 中的for..of语句用于对集合进行迭代,它会遍历集合的元素。与 JavaScript 中的for..in不同,for..of提供了一种访问可迭代对象的值的直接方法。

  • 当你将 TypeScript 代码编译成 JavaScript 时,可以指定目标版本。如果目标是 ECMAScript 2015(也就是 ES6)或更高版本,则生成的for..of代码将会使用原生 ES6 语法结构。

  • 如果目标版本低于 ES2015,TypeScript 编译器将使用等价的 ES5 代码来模拟for..of的行为。

下面是几个for..of的例子:

// 假设我们有一个数字数组
let numbers = [1, 2, 3];

// 使用 for..of 遍历数组中的每个数字
for (let number of numbers) {
  console.log(number); // 输出: 1, 2, 3
}
// 假设我们有一个字符串类型的数组
let pets = ["cat", "dog", "rabbit"];

// 使用 for..of 遍历数组中的每个字符串
for (let pet of pets) {
  console.log(pet); // 输出: cat, dog, rabbit
}

在上述例子中,如果编译目标是 ES2015 或更高版本,编译后的 JavaScript 代码将保持for..of结构。如果目标是低于 ES2015 的版本,如 ES5,则 TypeScript 编译器将转换为使用for循环和数组的.forEach方法或其他等效的 JavaScript 代码以便兼容老版本 JS 环境。