MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Typescript中的迭代器和生成器

2022-06-135.9k 阅读

迭代器(Iterator)

在 TypeScript 中,迭代器是一种设计模式,它提供了一种顺序访问聚合对象(如数组、对象等)中各个元素的方法,而不需要暴露该对象的内部表示。迭代器模式的核心思想是将遍历数据的逻辑从数据结构中分离出来,使得不同的数据结构可以使用相同的遍历方式。

迭代器协议

迭代器是一个对象,它实现了迭代器协议。迭代器协议定义了一个拥有 next() 方法的对象。每次调用 next() 方法时,迭代器会返回一个包含 valuedone 两个属性的对象:

  • value:表示当前迭代的值。
  • done:是一个布尔值,当迭代器遍历完所有值时为 true,否则为 false

下面是一个简单的手动实现迭代器的示例:

// 手动实现一个简单的迭代器
class MyIterator {
    private data: number[];
    private index: number = 0;

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

    next(): { value: number | undefined, done: boolean } {
        if (this.index < this.data.length) {
            const value = this.data[this.index];
            this.index++;
            return { value, done: false };
        } else {
            return { value: undefined, done: true };
        }
    }
}

// 使用自定义迭代器
const numbers = [1, 2, 3];
const myIterator = new MyIterator(numbers);
let result = myIterator.next();
while (!result.done) {
    console.log(result.value);
    result = myIterator.next();
}

在上述代码中,MyIterator 类实现了迭代器协议,next() 方法按照顺序返回数组中的值。通过不断调用 next() 方法,并检查 done 属性,我们可以遍历整个数组。

可迭代对象(Iterable)

可迭代对象是实现了可迭代协议的对象。可迭代协议要求对象必须实现一个 Symbol.iterator 方法,该方法返回一个迭代器对象。在 TypeScript 中,许多内置类型(如数组、字符串、Map 和 Set)都是可迭代对象。

下面是一个自定义可迭代对象的示例:

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

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

    [Symbol.iterator](): Iterator<number> {
        let index = 0;
        return {
            next(): { value: number | undefined, done: boolean } {
                if (index < this.data.length) {
                    const value = this.data[index];
                    index++;
                    return { value, done: false };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
}

// 使用自定义可迭代对象
const myIterable = new MyIterable([4, 5, 6]);
for (const value of myIterable) {
    console.log(value);
}

在这个例子中,MyIterable 类通过实现 Symbol.iterator 方法成为了一个可迭代对象。for...of 循环会自动调用 Symbol.iterator 方法获取迭代器,并使用迭代器来遍历对象中的值。

内置可迭代对象的迭代器

TypeScript 中的许多内置类型(如数组、字符串、Map 和 Set)都已经实现了可迭代协议。以数组为例:

const arr = [7, 8, 9];
const iterator = arr[Symbol.iterator]();
let arrResult = iterator.next();
while (!arrResult.done) {
    console.log(arrResult.value);
    arrResult = iterator.next();
}

上述代码通过获取数组的迭代器,手动遍历了数组。当然,实际开发中我们更常使用 for...of 循环来遍历数组:

const arr2 = [10, 11, 12];
for (const value of arr2) {
    console.log(value);
}

对于字符串,同样可以进行迭代:

const str = 'hello';
for (const char of str) {
    console.log(char);
}

Map 和 Set 也都是可迭代对象。对于 Map,迭代器会按插入顺序返回 [key, value] 数组:

const myMap = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of myMap) {
    console.log(key, value);
}

对于 Set,迭代器会按插入顺序返回集合中的值:

const mySet = new Set([13, 14, 15]);
for (const value of mySet) {
    console.log(value);
}

生成器(Generator)

生成器是一种特殊的函数,它可以暂停和恢复执行,并且可以在暂停时返回一个值。生成器函数使用 function* 语法定义,通过 yield 关键字暂停函数执行并返回一个值。

生成器函数

下面是一个简单的生成器函数示例:

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

const gen = myGenerator();
let genResult = gen.next();
while (!genResult.done) {
    console.log(genResult.value);
    genResult = gen.next();
}

在上述代码中,myGenerator 是一个生成器函数。当调用 myGenerator() 时,它并不会立即执行函数体,而是返回一个生成器对象。每次调用生成器对象的 next() 方法时,函数会执行到下一个 yield 语句处暂停,并返回 yield 后面的值。

生成器的状态和返回值

生成器对象有三种状态:

  • 未开始(unstarted):生成器函数已经定义,但 next() 方法还未被调用。
  • 活跃(active):生成器函数正在执行,即执行到 yield 暂停或者函数执行完毕。
  • 已完成(completed):生成器函数执行完毕,next() 方法返回的 done 属性为 true

生成器的 next() 方法不仅可以用于推进生成器执行,还可以接受一个参数。这个参数会作为上一个 yield 表达式的返回值。例如:

function* anotherGenerator() {
    const value1 = yield 'initial value';
    console.log('Received value:', value1);
    const value2 = yield 'second value';
    console.log('Received value:', value2);
}

const anotherGen = anotherGenerator();
let anotherGenResult = anotherGen.next();
console.log(anotherGenResult.value); // 输出: initial value
anotherGenResult = anotherGen.next(42);
console.log(anotherGenResult.value); // 输出: second value,同时在生成器内部,value1 的值为 42
anotherGenResult = anotherGen.next(100);
// 此时在生成器内部,value2 的值为 100,生成器执行完毕

在这个例子中,next(42)42 作为 yield 'initial value' 表达式的返回值,赋值给了 value1。同样,next(100)100 赋值给了 value2

生成器与迭代器的关系

生成器对象本身就是一个迭代器,它实现了迭代器协议。这意味着生成器可以直接在 for...of 循环中使用:

function* rangeGenerator(start: number, end: number) {
    for (let i = start; i < end; i++) {
        yield i;
    }
}

for (const num of rangeGenerator(5, 10)) {
    console.log(num);
}

上述代码定义了一个生成器函数 rangeGenerator,它生成从 startend - 1 的数字序列。通过 for...of 循环可以很方便地遍历这个生成器生成的序列。

生成器的高级应用

  1. 惰性求值:生成器可以实现惰性求值,即只有在需要时才计算值,而不是一次性计算所有值。这在处理大量数据或者计算复杂的数据序列时非常有用。例如,生成一个无限序列:
function* infiniteSequence() {
    let i = 0;
    while (true) {
        yield i++;
    }
}

const infiniteGen = infiniteSequence();
for (let j = 0; j < 10; j++) {
    console.log(infiniteGen.next().value);
}

在这个例子中,infiniteSequence 生成器函数可以生成一个无限的数字序列。通过 for 循环,我们只获取了前 10 个值,而不需要一次性生成无限个值。

  1. 异步操作的控制:生成器可以用于控制异步操作的流程。结合 yield 和 Promise,可以实现类似于同步代码风格的异步操作。例如:
function asyncTask() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Task completed');
        }, 1000);
    });
}

function* asyncGenerator() {
    const result1 = yield asyncTask();
    console.log(result1); // 输出: Task completed
    const result2 = yield asyncTask();
    console.log(result2); // 输出: Task completed
}

const asyncGen = asyncGenerator();
let asyncGenResult = asyncGen.next();
while (!asyncGenResult.done) {
    asyncGenResult.value.then((value) => {
        asyncGenResult = asyncGen.next(value);
    });
}

在这个例子中,asyncGenerator 生成器函数中通过 yield 暂停执行,等待 asyncTask 返回的 Promise 完成。当 Promise 完成后,将其结果作为 yield 表达式的返回值,继续执行生成器。

  1. 组合生成器:可以将多个生成器组合起来,形成更复杂的生成器。例如:
function* generator1() {
    yield 'a';
    yield 'b';
}

function* generator2() {
    yield 'c';
    yield 'd';
}

function* combinedGenerator() {
    yield* generator1();
    yield* generator2();
}

for (const value of combinedGenerator()) {
    console.log(value);
}

在上述代码中,combinedGenerator 通过 yield* 语法将 generator1generator2 的结果组合在一起。yield* 会将控制权委托给另一个生成器,使得组合生成器可以无缝地遍历多个生成器的结果。

迭代器与生成器在实际开发中的应用

数据处理和转换

在数据处理过程中,迭代器和生成器可以用于对数据进行逐行或逐个元素的处理和转换。例如,假设我们有一个包含大量用户数据的数组,需要对每个用户的年龄进行验证并转换为特定格式:

interface User {
    name: string;
    age: number;
}

const users: User[] = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 },
    { name: 'Charlie', age: 35 }
];

function* validateAndTransformUsers() {
    for (const user of users) {
        if (user.age >= 18) {
            yield `User ${user.name} is an adult.`;
        } else {
            yield `User ${user.name} is a minor.`;
        }
    }
}

for (const message of validateAndTransformUsers()) {
    console.log(message);
}

在这个例子中,validateAndTransformUsers 生成器函数对每个用户进行验证,并生成相应的消息。通过 for...of 循环可以方便地遍历这些消息。

数据流处理

在处理数据流(如文件流、网络流等)时,迭代器和生成器可以实现按需读取数据,避免一次性加载大量数据到内存中。例如,假设我们要逐行读取一个大文件:

// 模拟文件读取操作,返回一个 Promise 表示读取到的一行数据
function readLineFromFile(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('This is a line from the file');
        }, 1000);
    });
}

function* fileLineGenerator() {
    let line = yield readLineFromFile();
    while (line) {
        yield line;
        line = yield readLineFromFile();
    }
}

const fileGen = fileLineGenerator();
let fileGenResult = fileGen.next();
while (!fileGenResult.done) {
    fileGenResult.value.then((value) => {
        console.log(value);
        fileGenResult = fileGen.next(value);
    });
}

在这个例子中,fileLineGenerator 生成器函数通过 yield 暂停等待读取文件的一行数据。每次读取到数据后,将其返回并继续等待下一行数据。这样可以有效地处理大文件,而不会占用过多内存。

实现自定义集合和数据结构

通过实现迭代器和生成器,可以为自定义集合和数据结构提供统一的遍历接口。例如,我们可以实现一个自定义的双向链表,并为其提供迭代器:

class DoublyLinkedListNode<T> {
    value: T;
    prev: DoublyLinkedListNode<T> | null;
    next: DoublyLinkedListNode<T> | null;

    constructor(value: T) {
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}

class DoublyLinkedList<T> {
    private head: DoublyLinkedListNode<T> | null = null;
    private tail: DoublyLinkedListNode<T> | null = null;

    add(value: T) {
        const newNode = new DoublyLinkedListNode(value);
        if (!this.head) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            newNode.prev = this.tail;
            this.tail!.next = newNode;
            this.tail = newNode;
        }
    }

    [Symbol.iterator](): Iterator<T> {
        let current = this.head;
        return {
            next(): { value: T | undefined, done: boolean } {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return { value, done: false };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
}

const list = new DoublyLinkedList<number>();
list.add(1);
list.add(2);
list.add(3);

for (const num of list) {
    console.log(num);
}

在这个例子中,DoublyLinkedList 类实现了 Symbol.iterator 方法,使其成为一个可迭代对象。这样,我们就可以使用 for...of 循环来遍历双向链表中的元素。

状态机实现

生成器可以用于实现状态机。状态机是一种用于描述对象在不同状态之间转换的模型。通过 yield 可以暂停状态机的执行,并根据不同的输入进入不同的状态。例如:

function* stateMachine() {
    let state = 'initial';
    while (true) {
        const input = yield state;
        if (state === 'initial') {
            if (input === 'event1') {
                state ='state1';
            }
        } else if (state ==='state1') {
            if (input === 'event2') {
                state ='state2';
            }
        } else if (state ==='state2') {
            if (input === 'event3') {
                state = 'final';
            }
        }
        if (state === 'final') {
            break;
        }
    }
    return'state machine completed';
}

const machine = stateMachine();
let machineResult = machine.next();
console.log(machineResult.value); // 输出: initial
machineResult = machine.next('event1');
console.log(machineResult.value); // 输出: state1
machineResult = machine.next('event2');
console.log(machineResult.value); // 输出: state2
machineResult = machine.next('event3');
console.log(machineResult.value); // 输出: state machine completed

在这个例子中,stateMachine 生成器函数实现了一个简单的状态机。通过 yield 返回当前状态,并接受输入来转换状态。当状态转换到 final 时,状态机结束。

总结

迭代器和生成器是 TypeScript 中强大的工具,它们为数据遍历、异步操作控制和自定义数据结构提供了灵活且高效的解决方案。迭代器通过实现迭代器协议,使得各种数据结构可以以统一的方式进行遍历。生成器作为一种特殊的函数,通过 yield 关键字实现了函数的暂停和恢复执行,并且可以方便地实现惰性求值、异步操作控制等高级功能。在实际开发中,合理运用迭代器和生成器可以提高代码的可读性、可维护性以及性能。无论是处理大数据集、实现自定义数据结构,还是控制异步流程,迭代器和生成器都能发挥重要作用。希望通过本文的介绍,你能对 TypeScript 中的迭代器和生成器有更深入的理解,并在实际项目中灵活运用它们。