介绍TypeScript的装饰器、泛型与异步特性。
除了面向对象的概念之外,TypeScript 语言还引入了许多高级语言特性,以帮助开发健壮的面向对象代码。这些功能包括装饰器(Decorator
)、泛型(Generic
)、承约(Promise
),以及在使用异步函数时使用 async
和 await
关键字。装饰器(Decorator
)允许在处理类定义时注入和查询元数据,以及以编程方式附加到定义类的行为的能力。泛型(Generic
)提供了一种编写例程的技术,其中使用的对象的确切类型直到运行时才知道。 承约(Promise
) 提供了以流畅的方式编写异步代码的能力。
在设计编写代码时,大多数编码技术都是水平或垂直对齐的。一种常见的水平编码技术是 n 层
(表示多层)架构,其中程序被分成处理用户界面(GUI或WEB)的视图层
、处理业务逻辑的应用层
以及处理数据访问的数据层
。一种快速发展的垂直编码技术是切面
服务,其中每个垂直切面代表一个有界上下文,例如“系统日志服务”。
装饰器与水平和垂直架构都相关,但在垂直架构的上下文中可能特别有价值。装饰器可用于处理切面
关注点,例如日志记录、授权或验证。如果使用得当,这种面向方面的编程风格可以最大限度地减少满足这些共享责任所需的代码。TypeScript 装饰器可用于面向方面的编程 (AOP
) 并提供元
编程机制。
TypeScript 中的装饰器提供了一种以编程方式进入定义类的过程的方法。装饰器允许我们将代码注入到类的实际定义中,装饰器可用于类定义
、类属性
、类函数方法
,甚至函数方法参数
。
定义一个简单的类装饰器,如下所示:
function myDecorator(constructor : Function) {
console.log('myDecorator 已访问;');
}
定义了一个名为 myDecorator
的函数,它接受一个参数,名为 Function
类型的构造函数。 这个 myDecorator
函数只是将一条消息记录到控制台,表明它已被调用。 这个函数就是自我的
装饰器定义。 使用它,需要将之应用到类定义中,如下所示:
@myDecorator
class AbcWithDecorator {
}
使用 @
符号将装饰器应用于 AbcWithDecorator
的类定义,后跟装饰器名称。装饰器是在定义类时应用的,而不是在实例化时应用的。装饰器仅在定义类时被调用。
多装饰器
意味着可以将多个装饰器一个接一个地应用于同一个目标。考虑下面的代码:
function myFirstDecorator(constructor : Function) {
console.log('装饰器1被调用。')
}
function mySecondDecorator(constructor : Function) {
console.log('装饰器2被调用。')
}
@myFirstDecorator
@mySecondDecorator
class AbcWithMultipleDecorators {
}
装饰器按照它们在代码中出现的顺序进行评估,然后以相反的顺序调用。
类装饰器
将使用已装饰类的构造函数调用。在声明类时,JavaScript 运行时会自动调用装饰器函数,装饰器函数都被定义为接受一个名为构造函数的单个参数。考虑以下代码:
function mylog(constructor : any) {
console.log('mylog被调用。')
const original = constructor;
// 用日志构造函数包装构造函数
const constr: any = (...args) => {
console.log(`创建新的 ${original.name}实例`);
const c: any = () => {
return original.apply(null, args);
}
c.prototype = original.prototype;
return new c();
}
constr.prototype = original.prototype;
return constr;
}
@mylog
class MyCalculator {
rectangle(a: number, b: number) {
return a * b;
}
}
let cl1 = new MyCalculator();
let cl2 = new MyCalculator();
这里的类装饰器只传递一个参数MyCalculator
,表示被装饰类的构造函数。执行代码显示:
mylog被调用。
创建新的 MyCalculator实例
创建新的 MyCalculator实例
属性装饰器
是可用于类属性的装饰器函数。 使用两个参数调用属性装饰器——类原型本身
和属性名称
。考虑以下代码:
function mylog(target: any, key: string) {
let value = target[key];
// 替换 getter
const getter = function () {
console.log(`针对 ${key}的Getter,返回 ${value}`);
return value;
};
// 替换 setter
const setter = function (newVal) {
console.log(`设置 ${key} 值为 ${newVal}`);
value = newVal;
};
// 替换属性
if (delete this[key]) {
Object.definePrope rty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
class MyCalculator {
@mylog
public radius: number;
circle() {
let area = Math.PI * this.radius * this.radius;
console.log(area);
return area;
}
}
const mcl = new MyCalculator();
mcl.radius = 8;
mcl.circle();
每次调用属性的 getter
或 setter
时,都会记录访问。显示执行结果:
设置 radius 值为 8
针对 radius的Getter,返回 8
针对 radius的Getter,返回 8
201.06192982974676
方法装饰器是可以应用于类上的方法的装饰器。 JavaScript 运行时使用三个参数调用方法装饰器。 请记住
,类装饰器只有一个参数(类原型
),而属性装饰器有两个参数(类原型和属性名称
)。 方法装饰器具有三个参数(类原型
、方法名称
和方法描述符
[可选])。 第三个参数,方法描述符仅在为 ES5 及更高版本编译时才会填充。考虑以下代码:
function log(target: any, methodName: string, descriptor?: any) {
console.log(`类原型: ${JSON.stringify(target)}`);
console.log(`方法名 : ${methodName}`);
console.log(`target[methodName] : ${target[methodName]}`);
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
// 调用原始方法
const result = original.apply(this, args);
// 日志调用和结果
console.log(`${methodName} 带有参数 ${JSON.stringify(args)} 返回
${JSON.stringify(result)}`);
// 返回结果
return result;
}
return descriptor;
}
class Calculator {
// 使用装饰器
@log
rectangle(a: number, b: number) {
return a * b;
}
}
const calc = new Calculator();
calc.rectangle(2,3);
calc.rectangle(5,6);
在这里,定义了一个名为 log
的方法装饰器,它接受的三个参数:target
、methodName
和descriptor
。 请注意,descriptor
属性已被标记为可选。 装饰器中的前两行只是将 target
和 methodName
的值记录到控制台。 但是,请注意此装饰器的第三行。 在这里,将 target[methodName]
的值记录到控制台。 这会将实际的函数方法定义记录到控制台。
然后定义了一个名为 Calculator
的类。 这个类有一个rectangle
方法,它接受两个名为 a
和 b
的数字类型的参数。 该方法只是返回计算矩形的面积。 rectangle
方法已使用 log
装饰器进行了装饰。 这段代码的执行输出如下:
类原型: {}
方法名 : rectangle
target[methodName] : function (a, b) {
return a * b;
}
rectangle 带有参数 [2,3] 返回 6
rectangle 带有参数 [5,6] 返回 30
参数装饰器用于装饰特定方法的参数。考虑以下代码:
function log(target: any, methodName: string, parameterIndex: number) {
console.log(`类原型: ${JSON.stringify(target)}`);
console.log(`方法名 : ${methodName}`);
console.log(`参数索引 : ${parameterIndex}`);
}
class Calculator {
// 使用装饰器
rectangle(@log a: number, @log b: number) {
return a * b;
}
}
在这里,定义了一个名为 log
的函数,带有三个参数。 target
参数将包含之前看到的类原型。 methodName
参数将包含包含被装饰参数的方法的名称,parameterIndex
参数将包含参数的索引。 可以使用这个参数装饰器函数。然后定义了一个名为 Calculator
的类,它包含一个rectangle
方法。 此rectangle
方法有两个名为 a
和 b
的参数,它们是数字类型。 用 log
装饰器装饰了这两个值参数。 请注意,使用参数装饰器 (@log
) 的语法与任何其他装饰器相同。 这段代码的输出如下:
类原型: {}
方法名 : rectangle
参数索引 : 1
类原型: {}
方法名 : rectangle
参数索引 : 0
从输出可以看出,a
参数的参数索引为0,b
参数的参数索引为1,它们是rectangle
方法上的参数。 target
参数是一个类原型。
请注意,没有得到任何关于正在装饰的参数的信息。 不知道它是什么类型,甚至不知道它的名字是什么。 因此,参数装饰器只能真正用于找出已在方法上声明的参数。
泛型是一种编写代码的方式,可以处理任何类型的对象,但仍保持对象类型的完整性。 到现在为止,前面已经描述了接口、类和 TypeScript 的基本类型来确保示例中的代码是强类型。可以通过指定类型约束来约束程序使用的可能类型。在 TypeScript 中,可以创建泛型函数,包括泛型方法、泛型接口和泛型类。
作为 TypeScript 泛型语法的示例,这里编写一个名为 MyGeneric
的类,它将连接数组中的值。 需要确保数组的每个元素都是相同的类型。 这个类应该能够处理字符串数组、数字数组,事实上,是任何类型的数组。 为了做到这一点,需要依赖每种类型共有的特性。 由于所有 JavaScript
对象都有一个 toString 函数(运行时需要字符串时调用它),可以使用这个 toString 函数创建一个泛型类,该类输出数组中保存的所有值。
此 MyGeneric
类的泛型实现如下:
class MyGeneric< T > {
myArray(inputArray: Array< T >): string {
let returnString = "";
for (let i = 0; i < inputArray.length; i++) {
if (i > 0)
returnString += ",";
returnString += inputArray[i].toString();
}
return returnString;
}
}
首先注意到的是类声明的语法 MyGeneric<T>
。 这个<T>
语法是用来表示一个泛型类型的语法,在这个泛型类型使用的名字是T。myArray
函数也使用这个泛型类型语法 Array<T>
。 这表明 inputArray
参数必须是最初用于构造此类实例的类型的数组。
要使函数泛型,在函数名称后添加一个用尖括号 (< >
) 括起来的类型参数。 然后可以使用类型参数来注释函数参数、返回类型或函数中使用的类型(或其任何组合)。 调用泛型函数时,可以通过将类型参数放在函数名称后面的尖括号中来指定类型参数。 如果可以推断类型,则类型参数变为可选的。参考以下代码:
function reverse<T>(list: T[]) : T[] {
const reversedList: T[] = [];
for (let i = (list.length - 1); i >= 0; i--) {
reversedList.push(list[i]);
}
return reversedList;
}
const letters = ['张', '冠', '李', '戴'];
const reversedLetters = reverse<string>(letters);
const numbers = [5, 6, 7, 8];
const reversedNumbers = reverse<number>(numbers);
类型参数,编译器能够根据传递给函数的参数推断类型。执行输出如下:
字符串类型: ["戴","李","冠","张"]
数字类型: [8,7,6,5]
要创建泛型接口,类型参数
直接放在接口名称之后。 参考以下代码:
class PersonId {
constructor(private personIdValue: number) {
}
get value() {
return this.personIdValue;
}
}
class Person {
constructor(public id: PersonId, public name: string) {
}
}
interface IRepository<T, TId> {
getById(id: TId): T; persist(model: T): TId;
persist(model: T): TId;
}
class PersonRepository implements IRepository<Person, PersonId> {
constructor(private persons: Person[]) {
}
getById(id: PersonId) {
return this.persons[id.value];
}
persist(person: Person) {
this.persons[person.id.value] = person;
return person.id;
}
}
这里显示了一个通用的 IRepository
接口,它有两个类型参数,分别表示域对象的类型和该域对象的 ID 类型。 这些类型参数可以用作接口声明中的任何地方的注释。
当 PersonRepository
类实现泛型接口时,它提供具体的 Person
和 PersonId
类型作为类型参数。 检查 PersonRepository
类的主体以确保它实现了基于这些类型的接口。
泛型类可以通过提供单个实现来服务于许多不同类型的场景。 类型参数跟在类名之后,并用尖括号括起来。 类型参数可用于注释类中的方法参数、属性、返回类型和局部变量。参考以下代码:
class FieldId<T> {
constructor(private id: T) {
}
get value(): T {
return this.id;
}
}
class DingId extends FieldId<number> {
constructor(dingIdValue: number) {
super(dingIdValue);
}
}
class ZhangId extends FieldId<string> {
constructor(zhangIdValue: string) {
super(zhangIdValue);
}
}
const zhangId = new ZhangId('CODE-1');
const dingId = new DingId(8);
这里使用一个泛型类为域模型中的所有命名 ID
类型提供单一实现。 这允许命名所有 ID
,而不需要为每个命名类型单独实现。
承约(Promise
) 是一种用于标准化 JavaScript 中的异步处理的技术。用于异步处理的标准 JavaScript 技术是回调(callback
)机制。
引入了承约(Promise
)以减少由回调引起的几个问题。 使用回调链时,代码可能会变得嵌套很深且难以遵循。 当考虑错误处理时,回调通常会重复错误处理代码,进一步增加了理解程序的认知梯度。原生的承约(Promise
) 对象仅在ES5 之后的版本中可用。
承约(Promise
)是一个对象,它通过传入一个接受两个回调的函数来创建。 第一个回调用于指示响应成功,第二个回调用于指示错误响应。 考虑以下函数定义:
function fnPromise (
resolve: () => void,
reject : () => void){
function afterTimeout() {
resolve();
}
setTimeout( afterTimeout, 2000);
}
在这里,定义了一个名为 fnPromise
的函数,它接受两个函数作为参数。 这些函数被命名为resolve
和reject
,它们都返回一个void
。 在 fnPromise
函数的主体中,调用 setTimeout
(在函数的最后一行)等待两秒钟,然后再调用 resolve
回调函数。
现在可以使用这个函数来构造一个承约(Promise
)对象,如下:
function myResponsePromise() : Promise<void> {
return new Promise<void>(
fnPromise
);
}
此时创建了一个名为 myResponsePromise
的函数,它返回一个新的 Promise<void>
对象。 在函数体中,只是创建并返回一个新的 Promise
对象,并使用之前名为 fnPromise
的函数定义作为其构造函数中的唯一参数。 请注意,用于创建 Promise
的 void
类型使用通用语法 (new Promise<void>
) 来指示有关 Promise
的返回类型的一些信息。 稍后,当探讨如何从 Promise
中返回值时,将讨论通用 <void>
语法的使用。
在一般实践中,这两个函数定义组合在一个代码块中。 其目的是强调两个重要概念。 首先,要使用 Promise
,且必须返回一个新的 Promise
对象。 其次,Promise
对象是用一个带有两个回调参数的函数构造的。这两个步骤的组合实践如下:
function myPromise(): Promise<void> {
return new Promise<void>
(
(resolve: () => void,
reject: () => void
) => {
function afterTimeout() {
resolve();
}
setTimeout(afterTimeout, 2000);
}
);
}
在这里,有一个名为 myPromise
的函数,它返回一个新的 Promise<void>
对象。 该函数的第一行构造了新的 Promise
对象,并传入了一个匿名函数定义,该定义接受两个回调函数,名为 resolve
和 reject
。 然后在箭头 (=>
) 之后定义代码主体,并用匹配的大括号 {
和 }
括起来。 代码主体定义了一个名为 afterTimeout
的函数,该函数将在两秒超时后调用。 请注意,afterTimeout
函数正在调用 resolve
函数回调。
请记住,要使用 Promise
,必须构造并返回一个新的 Promise
对象,并且 Promise
对象的构造函数接受一个带有两个回调参数的函数(或匿名函数)。
Promise
提供了一种简单的语法来处理这些resolve
和reject
函数。 如何使用之前代码片段中定义的 Promise,如下所示:
function callPromise() {
console.log(`正在调用myPromise`);
myPromise().then(
() => { console.log(`myPromise.then()`) }
);
}
callPromise();
在这里,定义了一个名为 callPromise
的函数。 该函数将消息记录到控制台,然后调用myPromise
函数,使用附加到 Promise
对象的 then
函数,并定义了一个匿名函数,当 Promise
对象被解析(resolve
) 时将调用该函数。 这段代码的执行输出如下:
正在调用myPromise
myPromise.then()
这个承约(Promise)对象还定义了一个用于错误处理的catch
函数。 考虑以下定义:
function errorPromise(): Promise<void> {
return new Promise<void>
(
(resolve: () => void,
reject: () => void
) => {
reject();
}
);
}
在这里,使用承约(Promise)语法定义了一个名为 errorPromise
的函数。 请注意,在函数的主体中,调用的是reject
回调函数而不是 resolve
回调函数。 此reject
函数用于指示错误。 现在来使用 catch
函数来捕获这个错误,如下所示:
function callErrorPromise() {
console.log(`正在调用 errorPromise`);
errorPromise().then(
() => { console.log(`无错误!`) }
).catch(
() => { console.log(`有错误!`) }
);
}
callErrorPromise();
在这里,定义了一个名为 callErrorPromise
的函数,它将消息记录到控制台,然后调用 errorPromise
承约。 使用 then
响应中调用的匿名函数(即在成功时),还定义了一个在 catch
响应中调用的匿名函数(即在错误时)。 这段代码的执行输出如下:
正在调用 errorPromise
有错误!
博文最后更新时间: