TypeScript 类的基础:从定义到实例化,让你快速掌握(二)https://developer.aliyun.com/article/1426389
泛型类中使用泛型类型
在 TypeScript 中,泛型类可以使用泛型类型,即类型参数也可以是一个泛型类型。这样会使类更加灵活,可以应用于更多不同的场景中。
以下是一个使用泛型类型的泛型类示例:
class Pair<T, U> { private first: T; private second: U; constructor(first: T, second: U) { this.first = first; this.second = second; } public getFirst(): T { return this.first; } public getSecond(): U { return this.second; } } const pair1 = new Pair<string, number>('one', 1); // 创建一个存储字符串和数字的泛型类实例 console.log(pair1.getFirst()); // 输出 "one" console.log(pair1.getSecond()); // 输出 1 const pair2 = new Pair<number, boolean>(2, true); // 创建一个存储数字和布尔值的泛型类实例 console.log(pair2.getFirst()); // 输出 2 console.log(pair2.getSecond()); // 输出 true
在上述示例中,定义了一个泛型类 Pair
,其中类型参数 T
和 U
都是泛型类型。在泛型类实例化时,需要指定其中的类型参数,具体类型则被推断出。创建了两个泛型类实例,一个存储字符串和数字,另一个存储数字和布尔值,并获取其中的元素。
总之,在 TypeScript 中,泛型类可以使用泛型类型,即类型参数也可以是一个泛型类型。使用泛型类型可以使类更加灵活,可以应用于更多不同的场景中。在创建泛型类实例时,需要指定泛型类型的具体类型参数。
泛型约束
在 TypeScript 中,可以使用泛型约束来限制泛型类型的范围,避免在泛型类型使用时出现不合法的操作。泛型约束语法使用 extends
关键字,格式如下:
function 函数名<T extends 类型>(参数: T): void { // 函数实现 }
或者在类中使用泛型约束:
class 类名<T extends 类型> { // 类定义 }
以下是一个简单的泛型约束示例:
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // 输出参数的 length 属性 return arg; } loggingIdentity('hello'); // 输出 5 loggingIdentity([1, 2, 3]); // 输出 3
在上述示例中,定义了一个接口 Lengthwise
用来约束泛型类型的范围,并定义了一个使用了泛型约束的函数 loggingIdentity
。函数 loggingIdentity
的泛型参数 T
通过 extends Lengthwise
进行了约束,表示只有实现了 Lengthwise
接口的类型可以使用该函数。函数 loggingIdentity
接收一个参数 arg
,并输出该参数的 length
属性,并且返回该参数。最后,分别调用该函数,并传入字符串和数组,输出了它们的 length
属性。
总之,在 TypeScript 中,可以使用泛型约束来限制泛型类型的范围。泛型约束可以避免在泛型类型使用时出现不合法的操作。在泛型约束中,使用 extends
关键字进行约束,并指定一个类型范围。通过使用泛型约束,可以在函数或类中使用这些接口,来限制面向对象编程中的操作。
V. TypeScript 类的高级特性
类型别名和接口的使用场景与区别
在 TypeScript 中,类型别名和接口都是用来定义类型的工具,但它们的使用场景和特点不同。
类型别名(Type Alias)是给一个类型起一个新的名字,方便在其它地方复用,其语法格式为:
type 新类型名 = 基础类型 | 复合类型;
其中,基础类型
可以是任何已存在的 TypeScript 基本类型,包括 number
、string
、boolean
等;而 复合类型
则可以是对象、数组、枚举、元组等数据结构类型,或者其他已通过类型别名或接口定义的类型。
以下是一个使用类型别名的示例:
type Student = { name: string; age: number; grade: string; gender: 'M' | 'F'; };
在上述示例中,定义了一个名为 Student
的类型别名,用于表示一个学生对象。
接口(Interface)是定义一个数据结构的规范,它描述了一个对象应该具有哪些属性和方法,其语法格式为:
interface 接口名 { 属性1: 类型1; 属性2?: 类型2; // ? 表示属性可选 方法1?(参数: 类型): 返回类型; // ? 表示方法可选 }
以下是一个使用接口的示例:
interface Person { name: string; age: number; gender: 'M' | 'F'; getInfo(): string; }
在上述示例中,定义了一个名为 Person
的接口,并给出了一个简单的实例对象的类型定义。接口可以描述一个对象应该具有的属性和方法,而这些属性和方法可以由类或对象来实现。
区别:
- 类型别名可以定义基本类型或复杂类型,还可以定义联合类型、交叉类型等,更为灵活,但是不同的别名并不会自动合并。
- 接口只能定义对象或函数类型,不能定义基本类型,但可以被类或对象来实现。接口相当于一个契约,要求类或对象必须实现接口中定义的属性和方法,同时不同的接口会自动合并。
- 在使用时,如果需要描述一个类型的结构,应优先使用接口;如果需要对特定类型给一个(可能会经常使用)别名,应使用类型别名。
类装饰器的概念和应用
在 TypeScript 中,类装饰器是一种特殊的装饰器,用于修改类声明的行为。它们被应用于类声明之前,其语法格式为:@expression
,其中 expression
是一个函数,用于处理装饰器应用于类时的逻辑。
类装饰器的应用场景包括:
- 日志记录:在类的构造函数中添加日志记录逻辑,以便记录类实例的创建和销毁过程等,便于后续维护和调试。
- 权限验证:在类中添加权限验证逻辑,以便只有经过验证的用户可以访问类实例和其方法。
- 性能分析:在类中添加性能分析逻辑,以便统计类实例的方法被调用的次数和耗时等信息,便于优化类的性能。
- 功能扩展:在类中添加一些能够扩展类功能的方法或属性,以便实现接口的添加或版本升级等。
以下是一个简单的类装饰器示例:
function log<T extends { new (...args: any[]): {} }>(target: T): T { return class extends target { constructor(...args: any[]) { console.log(`类 ${target.name} 创建于 ${new Date().toLocaleString()}`); super(...args); console.log(`类 ${target.name} 销毁于 ${new Date().toLocaleString()}`); } }; } @log class Example { constructor(public name: string) {} } const ex = new Example('test'); console.log(ex.name);
在上述示例中,定义了一个名为 log
的类装饰器函数,它接收一个类的构造函数 target
作为参数,并返回一个新的被装饰后的类,并在其中添加了日志记录的逻辑。接着,使用 @log
进行装饰,对 Example
类进行日志记录,然后创建一个 Example
类的实例,并输出其名称属性。
总之,在 TypeScript 中,类装饰器是一种特殊的装饰器,用于修改类声明的行为。类装饰器的应用场景包括日志记录、权限验证、性能分析、功能扩展等。类装饰器的语法格式为 @expression
,其中 expression
是一个函数,用于处理装饰器应用于类时的逻辑。使用类装饰器需要注意遵循 TypeScript 类装饰器的规范,确保装饰器函数正确处理和返回类的构造函数。
VI. TypeScript 类的最佳实践
SOLID 原则
SOLID 是面向对象设计中五个基本的设计原则,它们共同以目标 OOP 的五个首字母 S、O、L、I、D 命名,具体含义如下:
- SRP(单一职责原则,Single Responsibility Principle):一个类或模块只负责一项职责,即只有一个引起它的变化的原因。
- OCP(开闭原则,Open Closed Principle):一个模块对于扩展是开放的,对于修改是封闭的。即在不修改源代码的情况下,可以通过扩展来增加新的功能。
- LSP(里氏替换原则,Liskov Substitution Principle):任何使用父类对象的地方,都可以无缝地使用其子类对象,即子类对象可以完全替换父类对象的位置。
- ISP(接口隔离原则,Interface Segregation Principle):一个类对于其依赖的其它类应该只暴露其它类真正需要使用的方法,而不是暴露所有的方法,即一个粗粒度的接口应该被切分成一些更细粒度的接口。
- DIP(依赖反转原则,Dependency Inversion Principle):高层模块不应该依赖于底层模块,它们应该共同依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
SOLID 原则的主要目的是提高代码的可维护性、可扩展性、可复用性和可读性等方面。这些设计原则可以帮助开发人员更好地把握实现细节、解决实际问题。在实践中,使用这些原则需要考虑到实际情况,避免过度设计和过度依赖框架等问题,使其真正拥有提高代码质量的价值。
类的单一职责原则
类的单一职责原则(Single Responsibility Principle,SRP)是 SOLID 原则中的一条,指一个类只应该有一个单一的责任,即只有一个引起它的变化的原因。
通俗地说,按照这一原则,每个类应该只负责一项功能,并且这项功能应该被封装在这个类内部。这个类中的所有方法和属性都应该为这一项功能服务,而不应该包含与这一项功能无关的内容。当需求变更时,该类也只会发生变更与该功能有关的代码,而不会影响到与该功能无关的代码。
例如,一个简单的账户类应该只包括账户相关的属性和方法,比如账户余额、账户转入转出的方法等。而不应该包括与账户功能无关的内容,比如发送电子邮件或短信的方法等。
遵循类的单一职责原则可以带来以下好处:
- 提高代码的可维护性:每个类只负责一项功能,代码逻辑更加清晰,易于理解和修改。
- 降低代码的复杂度:减少类的职责会使类的大小更小,代码复杂度更低。
- 提高代码的复用性:类的功能更加清晰明确,可以更容易地在不同的场景中被重用。
综上所述,类的单一职责原则是一条非常重要的设计原则,可以帮助开发人员在面向对象设计时更加专注于类自身的职责,从而提高代码的可维护性、可扩展性和可读性,使代码更好地适应变化。
类的依赖倒置原则
类的依赖倒置原则(Dependency Inversion Principle,DIP)是 SOLID 原则中的一条,指高层模块不应该依赖于底层模块,它们应该共同依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
简单来说,按照这一原则,一个类不应该直接依赖于另一个具体的类,而是应该依赖于一个抽象的接口或类。这样做有助于实现松偶合,从而提高代码的可维护性,降低耦合度。
这个原则可以通过以下三个步骤来实现:
- 定义抽象接口或类:定义一个抽象的接口或类,让高层模块依赖于抽象。
- 实现具体的接口或类:定义一个实现抽象接口或继承抽象类的具体实现类,提供具体的功能。
- 高层模块调用具体的接口或类:在高层模块中调用具体的接口或类。
例如,一个简单的购物车类,不应该直接依赖于具体的商品类,而是应该依赖于一个商品接口,并将具体的商品类实现这个接口。这样即使后续增加新的商品或删除商品,购物车的类也不需要做任何修改。
interface Product { getName(): string; getPrice(): number; } class Cart { private productList: Product[]; constructor(productList: Product[]) { this.productList = productList; } public getTotalPrice(): number { return this.productList.reduce((total, product) => total + product.getPrice(), 0); } } class Book implements Product { private name: string; private price: number; constructor(name: string, price: number) { this.name = name; this.price = price; } public getName(): string { return this.name; } public getPrice(): number { return this.price; } } class Food implements Product { private name: string; private price: number; constructor(name: string, price: number) { this.name = name; this.price = price; } public getName(): string { return this.name; } public getPrice(): number { return this.price; } } const cart = new Cart([new Book('《设计模式》', 39.9), new Food('橘子', 2.5)]); console.log(cart.getTotalPrice()); // 输出: 42.4
在上述示例中,定义了三个类:Product
接口、Cart
购物车类以及 Book
和 Food
具体的商品类。购物车类依赖于一个抽象商品类 Product
,在构造函数中传入具体的商品类,从而实现了类的依赖倒置原则。
总之,类的依赖倒置原则是面向对象编程中非常重要的一条原则,可以帮助我们实现松偶合,提高代码的可维护性、可扩展性和可读性,降低耦合度。正确地实践类的依赖倒置原则可以带来很多好处,包括更高的代码重用性、更好的代码扩展性等。
类的接口隔离原则
类的接口隔离原则(Interface Segregation Principle,ISP)是 SOLID 原则中的一条,指一个类对于其依赖的其它类应该只暴露其它类真正需要使用的方法,而不是暴露所有的方法,即一个粗粒度的接口应该被切分成一些更细粒度的接口。
简单来说,按照这一原则,一个类或模块不应该依赖于它不需要的接口,而应该将需要的接口拆分成各个功能单独的接口,只依赖于真正需要的接口。这样可以避免类或模块依赖不必要的接口,从而降低耦合度。
这个原则可以通过以下步骤来实现:
- 将接口拆分:将一个粗粒度的接口拆分成一些更细粒度的接口。
- 只依赖需要的接口:只使用需要的接口,而不是使用所有的接口。
例如,一个简单的会员类,不应该依赖于不需要的接口,而应该将自己需要的接口进行拆分,从而避免不必要的耦合。
interface MemberBasicInfo { getName(): string; } interface MemberBanInfo { isBanned(): boolean; } interface MemberPointInfo { getPoints(): number; } class Member implements MemberBasicInfo, MemberPointInfo { private name: string; private points: number; constructor(name: string, points: number) { this.name = name; this.points = points; } public getName(): string { return this.name; } public getPoints(): number { return this.points; } } const member = new Member('John', 100); console.log(member.getName(), member.getPoints()); // 输出: John 100
在上述示例中,定义了三个接口:MemberBasicInfo
、MemberBanInfo
和 MemberPointInfo
,它们分别提供了会员的基本信息、封禁状态和积分信息。而会员类 Member
则依赖于 MemberBasicInfo
和 MemberPointInfo
接口,只实现和使用了这两个接口,从而遵循了接口隔离原则。
总之,接口隔离原则是一个非常重要的设计原则,可以避免类和模块依赖不必要的接口,降低代码的耦合度,并提高代码的可维护性、可扩展性和可读性等方面。正确地实践类的接口隔离原则可以带来很多好处,包括更高的代码重用性、更好的代码扩展性等。
类的开闭原则
类的开闭原则(Open-Closed Principle,OCP)是 SOLID 原则中的一条,指一个模块对于扩展是开放的,对于修改是封闭的。即在不修改源代码的情况下,可以通过扩展来增加新的功能。
简单来说,按照这一原则,一个类应该对扩展开放,对修改关闭。也就是说,类的设计应该是稳定的,但又应该能够方便地扩展新的功能,而不是修改现有的代码。
这个原则可以通过以下步骤来实现:
- 抽象出稳定的接口:定义一个稳定的接口或抽象类,它可以是设计中的基础模块。
- 编写标准实现:实现具有良好扩展性的标准版本(标准实现)。
- 定义接口或抽象类的扩展点:定义接口或抽象类中的可扩展点,这些点可以是虚拟方法或事件。
- 编写扩展版本:从标准实现类继承,并增加新的方法扩展功能。
例如,一个简单的图形绘制系统,应该使用抽象类或接口来定义图形类,而不是具体的实现类,在这个基础上增加可以公用的方法,并定义可扩展点,从而保证了代码的稳定性,又能够方便地添加新功能。
abstract class Shape { protected lineWidth: number; constructor(lineWidth: number) { this.lineWidth = lineWidth; } public abstract draw(): void; } class Circle extends Shape { private radius: number; constructor(lineWidth: number, radius: number) { super(lineWidth); this.radius = radius; } public draw(): void { console.log(`Drawing a circle with the radius ${this.radius} and the line width ${this.lineWidth}`); } } class Rectangle extends Shape { private width: number; private height: number; constructor(lineWidth: number, width: number, height: number) { super(lineWidth); this.width = width; this.height = height; } public draw(): void { console.log(`Drawing a rectangle with the width ${this.width}, the height ${this.height} and the line width ${this.lineWidth}`); } } const shapes: Shape[] = [new Circle(1, 10), new Rectangle(2, 20, 30)]; shapes.forEach(shape => shape.draw()); // 输出: Drawing a circle with the radius 10 and the line width 1, Drawing a rectangle with the width 20, the height 30 and the line width 2
在上述示例中,定义了一个抽象的图形类 Shape
,并从它继承了 Circle
和 Rectangle
两个具体的图形类,都实现了 draw
方法。使用抽象类或接口来定义图形类,从而保证了代码的稳定性。如果需要添加新的图形,只需要继承 Shape
并实现新的 draw
方法即可,从而方便地扩展了新的功能。
总之,类的开闭原则是一个非常重要的设计原则,可以帮助开发人员编写具有良好扩展性和稳定性的代码。恰当地遵循类的开闭原则可以带来很多好处,比如提高代码复用性、降低代码修改成本等。
VII. 结论
强调类的重要性
在面向对象编程中,类是非常重要的一个概念。类是面向对象编程的基础,其作用远不止于定义对象的行为和属性。
在面向对象编程中,类还有以下几个重要的作用:
- 封装:类可以通过封装将对象的内部实现细节和外部进行隔离,从而隐藏对象的实现细节,只暴露对象对外提供的接口。这样可以避免外部代码直接操作对象内部的实现细节,提高了代码的安全性和可维护性。
- 继承:类可以通过继承来实现代码的复用。通过继承,一个类可以从另一个类中继承其属性和方法,并在此基础上添加自己的特殊属性和方法。这样能够避免重复编写代码,提高代码的复用性。
- 多态:类可以通过多态来提高代码的灵活性和扩展性。多态允许不同的对象以自己的方式响应相同的消息或调用相同的方法。这样可以让同一种消息或方法对于不同的对象产生不同的行为和反应,从而实现面向对象编程的多态性。
- 接口:类还可以通过实现接口来扩展类之间的协作关系。接口定义了类之间的协作规范,使得不同的类可以服从同一个接口来实现不同的功能,提高了代码的复用性和可扩展性。
综上所述,类是面向对象编程中非常重要的一个概念,可以帮助我们实现封装、继承、多态和接口等多种编程特性,从而提高代码的复用性、可维护性、可扩展性和可读性等多个方面。正确的使用和设计类能够带来很多好处,从而让我们更好地应对不同的编程需求和问题。
提出学习更多关于 TypeScript 和类的建议
以下是学习更多关于 TypeScript
和类的建议:
- 学习
TypeScript
基础知识:学习TypeScript
的语法和特性是学习类编程的基础。通过学习官方文档、教程或相关书籍,掌握TypeScript
基础语法和类型系统概念等。 - 练习
TypeScript
编程技能:学习程度不一的人可以编写各种类型的 TypeScript 代码。从简单的变量定义和函数到复杂的面向对象设计和实现,实践可以帮助你巩固和深入了解 TypeScript 和类。 - 学习类的面向对象设计:学习面向对象设计思想可以更好地理解和应用类编程的基础概念,例如封装、继承、多态和接口等。它还有助于您更好地设计和实现面向对象的类和应用。
- 推荐书籍和教程:推荐一些优秀的 TypeScript 和面向对象编程的书籍和在线教程,例如《TypeScript Deep Dive》、《深入浅出 TypeScript》、《设计模式:可复用面向对象软件的基础》等等。
- 学习业务应用:将 TypeScript 和类编程知识运用到实际业务场景中,例如开发一个 Web 应用程序或桌面应用程序等。这将有助于您更好地理解类和 TypeScript 编程,并加强您的编程技能。
- 参与社区:参与 TypeScript 社区讨论和开源项目等活动,与其他开发人员交流知识和成功经验。这将有助于您发现并了解 TypeScript 和类编程的新特性和应用场景,从而更好地发展自己的技能。
总之,学习 TypeScript 和类编程需要不断练习和学习,并积极参与社区活动。这样能够加强您的学习效果,提高您的编程技能和应用能力。