22、TypeScript 异常处理、内存管理、性能优化及使用 JavaScript 库指南

TypeScript 异常处理、内存管理、性能优化及使用 JavaScript 库指南

1. TypeScript 异常、内存与性能

在使用 TypeScript 编写大型应用时,有三个重要的方面需要考虑,分别是异常处理、内存管理和性能优化。这些方面往往是贯穿整个项目的关注点,在编写大量代码之前考虑它们,能避免后续不必要的修改。

1.1 异常处理

使用异常来处理程序中的真正异常状态,可以防止程序数据进一步损坏。我们可以创建自定义异常来管理不同类型的错误,并在 catch 块中测试异常类型,只处理那些我们知道可以恢复的错误。

  • 抛出异常 :可以使用 throw 关键字抛出任何对象,但最好使用自定义错误的子类。
  • 捕获异常 :可以使用 try-catch-finally 块来处理异常,其中 catch finally 块必须至少指定一个,也可以同时使用。
  • Promise 中的异常处理 :在处理 Promise 时,可以使用 catch 块、 finally 块或两者都用。
  • 自定义异常捕获 :虽然不能可靠地只捕获自定义异常,但可以在 catch 块中测试异常类型。
  • 资源管理 :大多数遇到的 API 遵循异步或类似 Promise 的模式,但如果需要管理资源,可以使用 try-finally 块进行清理。
  • 性能优化 :在进行性能优化时,需要在修改代码前后进行测量,以支持任何以优化为名的代码更改。
1.2 内存管理

现代运行时使用可靠的标记 - 清除算法来处理内存,避免了旧的引用计数垃圾回收器所面临的循环引用内存泄漏问题。一般来说,程序员在编码时不需要考虑垃圾回收,但如果发现性能问题是由垃圾回收引起的,可以尝试减少创建对象,以帮助垃圾回收器更好地工作。

1.3 性能优化

在进行性能优化时,首先要测量程序的性能,以验证关于优化的假设是否正确。还需要在多个环境中测量更改,确保在所有环境中都能提高速度。

2. 使用 JavaScript 库

JavaScript 社区在编写框架、工具包、实用函数和代码片段方面非常活跃。当搜索各种框架或工具包时,会发现有很多选择。然而,选择一个合适的库并非易事。

2.1 类型定义的重要性

在 TypeScript 程序中使用 JavaScript 库时,由于 TypeScript 编译器不了解 JavaScript 文件中提供的操作,需要提供类型定义来获得与 TypeScript 库相同级别的工具支持。类型定义用于编译器检查程序和语言服务提供自动完成功能,并且在编译时会被擦除,不会增加生产代码的负担。

例如,在纯 JavaScript 中使用 jQuery 时,典型的开发工作流程如下:
1. 输入选择器和方法,如 $('#elem').on(
2. 在 jQuery 文档中搜索 on 方法的签名
3. 在代码和文档之间切换以完成代码行

而通过包含 jQuery 的类型定义,可以获得智能自动完成功能,节省在编辑器和文档之间切换的时间。可以从 GitHub 上的官方类型定义仓库(Definitely Typed)获取类型定义,也可以使用以下命令从 NPM 安装:

npm install @types/jquery --save-dev
2.2 创建类型定义

以 Knockout 库为例,它是一个 MVVM 框架,通过将模型映射到视图来简化动态用户界面。

创建类型定义的步骤如下:
1. 检查已有定义 :在为流行库(如 Knockout)创建类型定义之前,先检查 Definitely Typed 项目或 NPM 上是否已有可用的定义(如 @types/knockout )。
2. 示例应用 :创建一个使用 Knockout 的 TypeScript 应用,包含一个 HTML 页面和一个 app.ts 文件。
- HTML 页面 :提供应用的视图,使用 data-bind 属性将视图模型绑定到 HTML 元素。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Knockout App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
</head>
<body>
    <h1>Your seat reservations (<span data-bind="text: seats().length"></span>)</h1>
    <table>
        <thead>
            <tr>
                <th>Passenger name</th>
                <th>Meal</th>
                <th>Surcharge</th>
                <th></th>
            </tr>
        </thead>
        <tbody data-bind="foreach: seats">
            <tr>
                <td><input data-bind="value: name" /></td>
                <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
                <td data-bind="text: formattedPrice"></td>
                <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>
            </tr>
        </tbody>
    </table>
    <button data-bind="click: addSeat, enable: seats().length < 5">Reserve another seat</button>
    <h2 data-bind="visible: totalSurcharge() > 0">
        Total surcharge: $<span data-bind="text: totalSurcharge().toFixed(2)"></span>
    </h2>
    <script src="knockout.js"></script>
    <script src="app.js"></script>
</body>
</html>
- **app.ts 文件**:包含使用 Knockout 绑定数据到视图的代码。
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);
    self.formattedPrice = ko.computed(function () {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";
    });
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;
    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealName: "Standard (sandwich)", price: 0 },
        { mealName: "Premium (lobster)", price: 34.95 },
        { mealName: "Ultimate (whole zebra)", price: 290 }
    ];
    // Editable data
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);
    // Computed data
    self.totalSurcharge = ko.computed(function () {
        var total = 0;
        for (var i = 0; i < self.seats().length; i++)
            total += self.seats()[i].meal().price;
        return total;
    });
    // Operations
    self.addSeat = function () {
        self.seats.push(new SeatReservation("", self.availableMeals[0]));
    }
    self.removeSeat = function (seat) { self.seats.remove(seat) }
}
ko.applyBindings(new ReservationsViewModel(), document.body);
  1. 消除编译器错误 :如果将这些文件放入开发环境,由于 TypeScript 编译器不认识 Knockout 的 ko 变量,会收到一些错误。可以使用以下类型定义来消除这些错误:
declare var ko: any;

但这种方法会关闭编译器的检查,并且无法获得自动完成功能。

  1. 逐步改进类型定义 :可以逐步编写类型定义,根据每次改进所带来的类型检查和自动完成的好处,决定投入多少精力。

    • 一级类型定义 :为应用中使用的所有一级属性提供类型信息,如 applyBindings computed observable observableArray
interface Knockout {
    applyBindings: any;
    computed: any;
    observable: any;
    observableArray: any;
}
declare var ko: Knockout;
- **细化类型定义**:参考库的官方文档,增加类型定义的详细程度。例如,`applyBindings` 方法的更新定义如下:
interface Knockout {
    applyBindings(viewModel: {}, rootNode?: HTMLElement): void;
    computed: any;
    observable: any;
    observableArray: any;
}
declare var ko: Knockout;
- **根据使用情况推断类型**:根据自己对库的使用情况推断类型,更新类型定义。例如,`computed` 方法的更新定义如下:
interface Knockout {
    applyBindings(viewModel: any, rootNode?: any): void;
    computed: (evaluator: () => any) => any;
    observable: any;
    observableArray: any;
}
declare var ko: Knockout;
- **完整的二级类型定义**:继续扩展定义,创建包含一级和二级类型信息的 `Knockout` 接口。
interface Knockout {
    applyBindings(viewModel: {}, rootNode?: HTMLElement): void;
    computed: (evaluator: () => any) => any;
    observable: (value: any) => any;
    observableArray: (value: any[]) => any;
}
declare var ko: Knockout;
- **拆分类型定义**:当类型定义变得难以管理时,可以使用额外的接口进行拆分,以限制定义的复杂度。
interface KnockoutApplyBindings {
    (viewModel: {}, rootNode?: HTMLElement): void;
}
interface Knockout {
    applyBindings: KnockoutApplyBindings;
    computed: (evaluator: () => any) => any;
    observable: (value: any) => any;
    observableArray: (value: any[]) => any;
}
declare var ko: Knockout;
3. 转换 JavaScript 应用

如果有一个现有的 JavaScript 应用并打算切换到 TypeScript,可以采用以下三种策略来处理旧代码:

策略 描述 优缺点
编写类型定义 为自己的 JavaScript 代码编写类型定义 缺点是会重复或浪费大量精力
将 JavaScript 文件添加到编译中 允许编译器包含 JavaScript 代码,开始将代码迁移到 TypeScript 的过程 可以让编译器在很多情况下推断类型信息
将 JavaScript 代码添加到 TypeScript 文件中 将 JavaScript 代码移动到 TypeScript 文件中,完成迁移过程 可以添加类型注解来改进类型信息,辅助编译器理解类型

以下是一个简单的示例,展示如何将 JavaScript 代码集成到 TypeScript 项目中。

旧的 JavaScript 文件( myoldlib.js

function old_process(name) {
    return name + ' processed';
}

新的 TypeScript 代码( mynewlib.ts

class NewProcessor {
    process(name: string) {
        return old_process(name);
    }
}

当尝试编译 mynewlib.ts 时,会出现错误 Cannot find name 'old_process' ,因为编译器看不到旧代码。可以使用以下命令将 JavaScript 文件包含在编译中:

tsc myoldlib.js mynewlib.ts --allowJs --outDir ./dist

还可以在 TypeScript 文件中显式引用 JavaScript 依赖:

///<reference path="myoldlib.js" />
class NewProcessor { 
    process(name: string) {
        return old_process(name);
    }
}

这样,在编译时就不需要再指定 JavaScript 文件,并且编辑器会提供推断的类型信息作为自动完成提示:

tsc mynewlib.ts --allowJs --outDir ./dist

如果有大量 JavaScript 文件需要升级,可以先将低级依赖升级到 TypeScript,而其他依赖它们的 JavaScript 文件继续引用 TypeScript 文件的编译输出。在运行时,只要只添加类型注解而不重构程序,文件最初是用 TypeScript 还是 JavaScript 编写并没有区别。最好在整个程序都用 TypeScript 编写后再进行重构,因为 TypeScript 的重构支持更智能。

总结

通过合理处理异常、管理内存和优化性能,以及正确使用 JavaScript 库和类型定义,可以提高 TypeScript 应用的质量和开发效率。在实际开发中,要根据具体情况选择合适的方法和策略,不断优化代码,以满足项目的需求。

流程图

graph TD;
    A[开始使用 JavaScript 库] --> B{是否有可用的类型定义};
    B -- 有 --> C[安装类型定义];
    B -- 没有 --> D[创建类型定义];
    D --> E[一级类型定义];
    E --> F[细化类型定义];
    F --> G[根据使用情况推断类型];
    G --> H[完整的二级类型定义];
    H --> I[拆分类型定义];
    C --> J[在 TypeScript 中使用库];
    I --> J;
    K[现有 JavaScript 应用] --> L{选择处理策略};
    L -- 编写类型定义 --> M[编写类型定义文件];
    L -- 添加到编译中 --> N[使用 --allowJs 编译];
    L -- 添加到 TypeScript 文件 --> O[迁移代码并添加类型注解];
    M --> P[在 TypeScript 中使用旧代码];
    N --> P;
    O --> P;

TypeScript 异常处理、内存管理、性能优化及使用 JavaScript 库指南

4. 详细操作步骤总结

为了更清晰地展示在 TypeScript 项目中处理异常、使用 JavaScript 库以及迁移旧 JavaScript 代码的操作流程,下面将详细总结各个关键步骤。

4.1 异常处理操作步骤
  • 抛出异常 :使用 throw 关键字,优先选择自定义错误子类,示例如下:
class CustomError extends Error {
    constructor(message: string) {
        super(message);
        this.name = 'CustomError';
    }
}

try {
    throw new CustomError('This is a custom error');
} catch (error) {
    if (error instanceof CustomError) {
        console.log(error.message);
    }
}
  • 捕获异常 :使用 try-catch-finally 块, catch finally 块至少指定一个。
try {
    // 可能抛出异常的代码
    const result = 1 / 0;
} catch (error) {
    console.log('An error occurred:', error);
} finally {
    console.log('This will always execute');
}
  • Promise 中的异常处理 :使用 catch finally 块。
function asyncOperation() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('Async operation failed'));
        }, 1000);
    });
}

asyncOperation()
  .catch(error => {
        console.log('Promise error:', error);
    })
  .finally(() => {
        console.log('Promise operation completed');
    });
4.2 使用 JavaScript 库操作步骤
  • 获取类型定义
    • 对于流行库,从 NPM 安装,如 jQuery:
npm install @types/jquery --save-dev
- 对于没有现有定义的库,手动创建。
  • 创建类型定义 :以 Knockout 为例:
    1. 检查是否已有定义,如 @types/knockout
    2. 创建示例应用,包含 HTML 和 app.ts 文件。
    3. 消除编译器错误,使用 declare var ko: any;
    4. 逐步改进类型定义:
      • 一级类型定义:
interface Knockout {
    applyBindings: any;
    computed: any;
    observable: any;
    observableArray: any;
}
declare var ko: Knockout;
    - 细化类型定义,参考官方文档:
interface Knockout {
    applyBindings(viewModel: {}, rootNode?: HTMLElement): void;
    computed: any;
    observable: any;
    observableArray: any;
}
declare var ko: Knockout;
    - 根据使用情况推断类型:
interface Knockout {
    applyBindings(viewModel: any, rootNode?: any): void;
    computed: (evaluator: () => any) => any;
    observable: any;
    observableArray: any;
}
declare var ko: Knockout;
    - 完整的二级类型定义:
interface Knockout {
    applyBindings(viewModel: {}, rootNode?: HTMLElement): void;
    computed: (evaluator: () => any) => any;
    observable: (value: any) => any;
    observableArray: (value: any[]) => any;
}
declare var ko: Knockout;
    - 拆分类型定义:
interface KnockoutApplyBindings {
    (viewModel: {}, rootNode?: HTMLElement): void;
}
interface Knockout {
    applyBindings: KnockoutApplyBindings;
    computed: (evaluator: () => any) => any;
    observable: (value: any) => any;
    observableArray: (value: any[]) => any;
}
declare var ko: Knockout;
4.3 迁移 JavaScript 代码操作步骤
  • 将 JavaScript 文件添加到编译中:
tsc myoldlib.js mynewlib.ts --allowJs --outDir ./dist
  • 在 TypeScript 文件中显式引用 JavaScript 依赖:
///<reference path="myoldlib.js" />
class NewProcessor {
    process(name: string) {
        return old_process(name);
    }
}
  • 编译时不再指定 JavaScript 文件:
tsc mynewlib.ts --allowJs --outDir ./dist
5. 注意事项和最佳实践

在使用 TypeScript 进行开发时,除了掌握上述操作步骤,还需要注意以下事项和遵循一些最佳实践。

5.1 异常处理注意事项
  • 精确捕获异常 :在 catch 块中,尽量精确地捕获特定类型的异常,避免捕获所有异常而掩盖潜在问题。
  • 避免空 catch :空的 catch 块会忽略异常,导致难以调试,应至少记录异常信息。
5.2 使用 JavaScript 库注意事项
  • 及时更新类型定义 :随着 JavaScript 库的更新,类型定义可能会过时,及时更新以获得最新的类型检查和自动完成功能。
  • 选择合适的库 :在众多 JavaScript 库中选择时,要综合考虑功能、性能、社区支持等因素。
5.3 迁移 JavaScript 代码最佳实践
  • 逐步迁移 :对于大型项目,不要一次性将所有 JavaScript 代码迁移到 TypeScript,应逐步进行,先处理低级依赖。
  • 添加类型注解 :在迁移过程中,及时添加类型注解,提高代码的可读性和可维护性。
6. 总结与展望

通过本文的介绍,我们了解了 TypeScript 中异常处理、内存管理、性能优化的重要性,以及如何使用 JavaScript 库和迁移旧的 JavaScript 代码。合理运用这些技术,可以提高 TypeScript 应用的质量和开发效率。

未来,随着 TypeScript 的不断发展和 JavaScript 生态系统的日益丰富,我们可以期待更多高效的开发工具和技术出现。同时,在实际项目中,要不断总结经验,灵活运用各种方法,以适应不同的项目需求。

流程图

graph TD;
    A[TypeScript 项目开发] --> B{是否需要处理异常};
    B -- 是 --> C[使用 try-catch-finally 块];
    B -- 否 --> D{是否使用 JavaScript 库};
    C --> D;
    D -- 是 --> E{是否有类型定义};
    E -- 有 --> F[安装并使用类型定义];
    E -- 没有 --> G[创建类型定义];
    G --> H[逐步完善类型定义];
    F --> I[在 TypeScript 中使用库];
    H --> I;
    D -- 否 --> J{是否有旧 JavaScript 代码迁移};
    I --> J;
    J -- 是 --> K{选择迁移策略};
    K -- 编写类型定义 --> L[编写类型定义文件];
    K -- 添加到编译中 --> M[使用 --allowJs 编译];
    K -- 添加到 TypeScript 文件 --> N[迁移代码并添加类型注解];
    L --> O[在 TypeScript 中使用旧代码];
    M --> O;
    N --> O;
    J -- 否 --> P[完成项目开发];
    O --> P;
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值