理解 TypeScript 生成器
作者:Debjyoti Banerjee ✏️
普通函数从上到下运行,然后退出。生成器函数也从上到下运行,但它们可以在执行过程中暂停,稍后从同一点恢复。这个过程一直持续到进程结束,然后退出。在本文中,我们将学习如何在 TypeScript 中使用生成器函数,并涵盖一些不同的示例和用例。让我们开始吧!
向前跳转:
在 TypeScript 中创建生成器函数
普通函数是主动执行的,而生成器是被动执行的,这意味着它们可以在稍后的时间点被调用执行。要创建生成器函数,我们将使用function *
命令。生成器函数看起来与普通函数类似,但行为略有不同。请看以下示例:
function normalFunction() {
console.log("This is a normal function");
}
function* generatorFunction() {
console.log("This is a generator function");
}
normalFunction(); // "This is a normal function"
generatorFunction();
虽然它的编写和执行方式与普通函数一样,generatorFunction
但调用时控制台中不会显示任何日志。简而言之,调用生成器并不会执行代码:你会注意到生成器函数返回一个
Generator
类型;我们将在下一节详细讨论这一点。为了让生成器执行我们的代码,我们将执行以下操作:
function* generatorFunction() {
console.log("This is a generator function");
}
const a = generatorFunction();
a.next();
请注意,该next
方法返回一个IteratorResult
。因此,如果我们要从 返回一个数字generatorFunction
,我们将按如下方式访问该值:
function* generatorFunction() {
console.log("This is a generator function");
return 3;
}
const a = generatorFunction();
const b = a.next();
console.log(b); // {"value": 3, "done": true}
console.log(b.value); // 3
生成器接口扩展了Iterator
,这允许我们调用next
。它还具有[Symbol.iterator]
属性,使其成为可迭代的。
理解 JavaScript 可迭代对象和迭代器
可迭代对象是可以使用 进行迭代的对象for..of
。它们必须实现Symbol.iterator
方法;例如,JavaScript 中的数组是内置的可迭代对象,因此它们必须具有迭代器:
const a = [1,2,3,4];
const it: Iterator<number> = a[Symbol.iterator]();
while (true) {
let next = it.next()
if (!next.done) {
console.log(next.value)
} else {
break;
}
}
迭代器使得迭代可迭代对象成为可能。请看下面的代码,这是一个非常简单的迭代器实现:
>function naturalNumbers() {
let n = 0;
return {
next: function() {
n += 1;
return {value:n, done:false};
}
};
}
const iterable = naturalNumbers();
iterable.next().value; // 1
iterable.next().value; // 2
iterable.next().value; // 3
iterable.next().value; // 4
如上所述,可迭代对象是具有Symbol.iterator
属性的对象。因此,如果我们像上例一样,将一个返回该next()
函数的函数赋值给它,我们的对象就会变成 JavaScript 可迭代对象。然后,我们就可以使用以下语法对其进行迭代for..of
。
显然,我们之前看到的生成器函数与上面的例子有相似之处。事实上,由于生成器一次只计算一个值,我们可以很容易地使用生成器来实现迭代器。
在 TypeScript 中使用生成器
生成器的一大亮点在于,你可以使用yield
语句暂停执行,而我们在之前的示例中并没有这样做。当next
被调用时,生成器会同步执行代码,直到yield
遇到 ,此时它会暂停执行。如果next
再次调用 ,它将从暂停的地方继续执行。我们来看一个例子:
function* iterator() {
yield 1
yield 2
yield 3
}
for(let x of iterator()) {
console.log(x)
}
yield
基本上允许我们从函数中返回多次。此外,数组永远不会在内存中创建,这使我们能够以非常节省内存的方式创建无限序列。以下示例将生成无限偶数:
function* evenNumbers() {
let n = 0;
while(true) {
yield n += 2;
}
}
const gen = evenNumbers();
console.log(gen.next().value); //2
console.log(gen.next().value); //4
console.log(gen.next().value); //6
console.log(gen.next().value); //8
console.log(gen.next().value); //10
我们还可以修改上面的示例,以便它接受一个参数并从提供的数字开始产生偶数:
function* evenNumbers(start: number) {
let n = start;
while(true) {
if (start === 0) {
yield n += 2;
} else {
yield n;
n += 2;
}
}
}
const gen = evenNumbers(6);
console.log(gen.next().value); //6
console.log(gen.next().value); //8
console.log(gen.next().value); //10
console.log(gen.next().value); //12
console.log(gen.next().value); //14
TypeScript 生成器的用例
生成器提供了一种强大的机制,用于控制数据流并在 TypeScript 中创建灵活、高效且可读的代码。它能够按需生成值、处理异步操作以及创建自定义迭代逻辑,这使得它们在某些场景下成为一种非常实用的工具。
按需计算值
您可以实现生成器来按需计算并生成值,并缓存中间结果以提高性能。这种技术在处理昂贵的计算或将某些操作延迟到实际需要时非常有用。让我们考虑以下示例:
function* calculateFibonacci(): Generator<number> {
let prev = 0;
let curr = 1;
yield prev;
yield curr;
while (true) {
const next = prev + curr;
yield next;
prev = curr;
curr = next;
}
}
// Using the generator to calculate Fibonacci numbers lazily
const fibonacciGenerator = calculateFibonacci();
// Calculate the first 10 Fibonacci numbers
for (let i = 0; i < 10; i++) {
console.log(fibonacciGenerator.next().value);
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
在上面的例子中,我们无需预先计算所有斐波那契数,而是在请求时仅计算并返回所需的斐波那契数。这样可以更高效地利用内存,并根据需要按需计算值。
迭代大型数据集
生成器允许您迭代大型数据集,而无需一次性将所有数据加载到内存中。相反,您可以根据需要生成值,从而提高内存效率。这在处理大型数据库或文件时尤其有用:
function* iterateLargeData(): Generator<number> {
const data = Array.from({ length: 1000000 }, (_, index) => index + 1);
for (const item of data) {
yield item;
}
}
// Using the generator to iterate over the large data set
const dataGenerator = iterateLargeData();
for (const item of dataGenerator) {
console.log(item);
// Perform operations on each item without loading all data into memory
}
在此示例中,iterateLargeData
生成器函数通过创建一个包含一百万个数字的数组来模拟大型数据集。生成器不会一次性返回整个数组,而是使用关键字一次生成一个元素yield
。因此,您可以迭代数据集,而无需将所有数字同时加载到内存中。
递归使用生成器
生成器的内存高效特性可以用于更实用的事情,例如递归读取目录中的文件名。事实上,当我想到生成器时,递归遍历嵌套结构对我来说是自然而然的事情。
由于yield
是一个表达式,因此yield*
可以用来委托给另一个可迭代对象,如下例所示:
function* readFilesRecursive(dir: string): Generator<string> {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
yield* readFilesRecursive(path.join(dir, file.name));
} else {
yield path.join(dir, file.name);
}
}
}
我们可以按如下方式使用我们的函数:
for (const file of readFilesRecursive('/path/to/directory')) {
console.log(file);
}
我们还可以使用yield
来将值传递给生成器。请看以下示例:
function* sumNaturalNumbers(): Generator<number, any, number> {
let value = 1;
while(true) {
const input = yield value;
value += input;
}
}
const it = sumNaturalNumbers();
it.next();
console.log(it.next(2).value); //3
console.log(it.next(3).value); //6
console.log(it.next(4).value); //10
console.log(it.next(5).value); //15
当next(2)
被调用时,input
被赋值2
;同样,当next(3)
被调用时, 输入 被赋值3
。
错误处理
如果要使用生成器,异常处理和执行流程控制是一个需要讨论的重要概念。生成器基本上看起来像普通函数,因此语法相同。
当生成器遇到错误时,可以使用throw
关键字抛出异常。可以使用生成器函数内部的块或使用生成器时在外部捕获和处理此异常:try...catch
function* generateValues(): Generator<number, void, string> {
try {
yield 1;
yield 2;
throw new Error('Something went wrong');
yield 3; // This won't be reached
} catch (error) {
console.log("Error caught");
yield* handleError(error); // Handle the error and continue
}
}
function* handleError(error: Error): Generator<number, void, string> {
yield 0; // Continue with a default value
yield* generateFallbackValues(); // Yield fallback values
throw `Error handled: ${error.message}`; // Throw a new error or rethrow the existing one
}
const generator = generateValues();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // Error caught
// { value: 0, done: false }
console.log(generator.next()); // { value: 4, done: false }
console.log(generator.next()); // Error handled: Something went wrong
在此示例中,generateValues
生成器函数在 yield 值后抛出错误2
。生成器中的 catch 块捕获了该错误,并将控制权转移到handleError
生成器函数,该函数会 yield 回退值。最后,该handleError
函数抛出新的错误或重新抛出现有错误。
try...catch
当使用生成器时,您也可以使用块捕获抛出的错误:
const generator = generateValues();
try {
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
} catch (error) {
console.error('Caught error:', error);
}
在这种情况下,错误将被 catch 块捕获,您可以进行相应的处理。
结论
在本文中,我们学习了如何在 TypeScript 中使用生成器,回顾了它们的语法以及 JavaScript 迭代器和可迭代对象的基础。我们还学习了如何递归使用 TypeScript 生成器以及如何使用生成器处理错误。
生成器可以用于许多有趣的用途,例如生成唯一 ID、生成素数或实现基于流的算法。您可以使用条件或手动跳出生成器来控制序列的终止。希望您喜欢这篇文章,如有任何疑问,请随时留言。
几分钟内即可设置 LogRocket 的现代 Typescript 错误跟踪:
- 访问https://logrocket.com/signup/获取应用程序 ID。
- 通过 npm 或脚本标签安装 LogRocket。
LogRocket.init()
必须在客户端调用,而不是在服务器端调用。
npm:
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
脚本标签:
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
3.(可选)安装插件以实现与您的堆栈的更深入集成:
- Redux 中间件
- ngrx中间件
- Vuex 插件