I. 引言
介绍 TypeScript 和面向对象编程
TypeScript
是一种开源的编程语言,是JavaScript
的超集,通过在JavaScript
上增加强类型、接口和其他关键特性来扩展它的功能。TypeScript
编译后会生成JavaScript
代码,从而可以在任何支持JavaScript
的环境中运行。相比于JavaScript
,TypeScript
更加健壮并且易于维护,尤其在大型项目中尤其重要。
面向对象编程(Object-Oriented Programming,简称OOP
)是一种编程范式,它将数据抽象为对象,并将数据和操作数据的行为统一封装在对象中,从而更好地实现模块化和抽象
。对于很多程序员来说,OOP是一个非常有用的工具箱,因为它提高了代码的可扩展性、可读性和可重用性。
TypeScript
和OOP
的结合让程序员能够自然地实现类、继承和接口,从而降低了开发大型应用程序的复杂性。TypeScript
中的类提供了封装、继承和多态性等关键特性,使得开发者能够用更好的方式来组织代码、管理状态和复用功能。同时,TypeScript
还提供了声明式类型系统和静态类型检查,可以大大减少在运行时出错的可能性,提高代码的可靠性和维护性。
总之,TypeScript
和面向对象编程的组合使得开发者能够更好地开发与维护大型应用程序,同时还提供了工具和方法来更好地理解和组织代码。
提出 TypeScript 中类的重要性
TypeScript
中的类是实现对象组织和抽象的关键部分,提供了封装、继承和多态
等关键特性,使开发者能够以更好的方式来管理状态、实现功能并组织代码。
- 类的封装性确保了数据和方法的访问权限,从而确保了代码的安全性和可靠性。这种封装性可以让数据或方法只在类内部使用,可以防止意外改变数据或方法,也可以更容易地在代码中进行重构。
- 继承特性使得一个类可以继承另一个类的特性和方法,并且可以在其上面实现更多功能。这个特性可以减少代码重复和提高代码复用率。
- 多态性使得同一个方法可以具有多种实现方式,这使得开发者可以为不同的类型写出共享方法,并且使得代码更加灵活和具有可扩展性。
另外,通过使用接口,可以更容易地定义类之间的通信和交互,从而降低了代码的耦合度并提高了代码的可维护性。
总之,TypeScript
中的类是最重要的语言特性之一,通过提供面向对象编程的组件以及强大的类型系统和静态类型检查,使得开发者能够更好地组织、维护和扩展大型应用程序。
简要介绍将要涉及的内容
本文将涉及 TypeScript 中类的基础、继承、泛型、高级特性、最佳实践等多个方面。
具体来说,将包括以下内容:
- 类的定义和实例化,成员变量和成员函数的使用;
- 访问修饰符和静态成员;
- 类的构造函数;
- 继承的概念和语法、子类的构造函数、子类中的成员变量和成员函数、重写父类的方法、抽象类和抽象方法、以及接口继承;
- 泛型类的定义和实例化、泛型约束、泛型类型参数;
- 类型别名和接口的应用场景和区别,以及类装饰器的概念和应用;
- SOLID 原则和各个原则在类中的应用,类的单一职责原则、类的依赖倒置原则、类的接口隔离原则、类的开闭原则。
通过学习这些内容,您将能更好地理解 TypeScript
类的语法和设计,以及如何在 TypeScript
中编写更好的代码。
II. TypeScript 类的基础
类的定义与实例化
在 TypeScript 中,类定义需要使用 class
关键字,类名通常首字母大写,成员变量和方法在类定义中使用 成员修饰符
来定义和限制其访问权限。
下面是一个简单的 TypeScript 类定义和实例化的示例:
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`); } } const person1 = new Person('Tom', 30); person1.sayHello(); // 输出 "Hello, my name is Tom, I'm 30 years old."
在上面的示例中,Person
类有两个成员变量 name
和 age
,一个构造函数 constructor
,以及一个成员函数 sayHello()
。构造函数负责初始化类的属性,而成员函数用于处理对象的行为。
创建 Person
类实例时,使用 new
操作符实例化一个对象(这里是 person1
),并向类构造函数传递构造参数值。调用 sayHello()
函数时,将显示类的成员变量值。
总之,TypeScript
中的类定义是标准的面向对象编程概念,并且增加了类型定义和强类型特性,使得其更加健壮和可维护。
成员变量
在 TypeScript
中,类的成员变量需要使用 成员修饰符
来限制其访问权限和可见性。
TypeScript
中有三种成员修饰符:
public
:公共修饰符,表示该属性或方法可从类的内部、子类和类的外部访问(默认为public
,可以省略);protected
:受保护修饰符,表示该属性或方法可从类的内部和子类访问;private
:私有修饰符,表示该属性或方法仅可从类的内部访问。
下面是一个示例代码,演示如何在 TypeScript
类中使用成员变量:
class Person { public name: string; // 公共成员变量 protected age: number; // 保护成员变量 private sex: string; // 私有成员变量 constructor(name: string, age: number, sex: string) { this.name = name; this.age = age; this.sex = sex; } sayHello() { console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old, and I'm ${this.sex}.`); } } const person1 = new Person('Tom', 30, 'male'); person1.sayHello(); // 输出 "Hello, my name is Tom, I'm 30 years old, and I'm male." console.log(person1.name); // 输出 "Tom" // console.log(person1.age); // 报错:Property 'age' is protected and only accessible within class 'Person' and its subclasses. // console.log(person1.sex); // 报错:Property 'sex' is private and only accessible within class 'Person'.
在上面的示例中,Person
类有三个成员变量 name
、age
和 sex
,分别为公共、受保护和私有访问级别。创建 Person
类的实例 person1
后,可以调用公共的 sayHello()
方法,访问公共的 name
变量,但是受保护和私有成员是无法从实例外部访问的。
总之,成员变量可以用来封装数据和状态,并使用修饰符来控制数据的可见性,以保证类的封装性和代码安全。
访问修饰符
在 TypeScript 中,可以使用访问修饰符来限制类成员变量的可访问性和可见性。
TypeScript 中的访问修饰符有三种:
public
:默认的访问修饰符。被它修饰的成员变量或方法可以在任何地方被访问。protected
:被这个修饰符修饰的成员变量或方法可以在声明它的类及其子类中被访问。private
:被这个修饰符修饰的成员变量或方法只能在声明它的类中被访问。
下面是一个简单的 TypeScript
类示例代码,其中使用了不同的访问修饰符:
class Person { public name: string; // 公共成员变量,任何位置都可以访问 protected age: number; // 受保护成员变量,只有在类内部和子类中可以访问 private sex: string; // 私有成员变量,只有在类内部可以访问 constructor(name: string, age: number, sex: string) { this.name = name; this.age = age; this.sex = sex; } public sayHello() { // 公共成员方法,任何位置都可以访问 console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old, and I'm ${this.sex}.`); } protected getSurname() { // 受保护成员方法,只有在类内部和子类中可以访问 return this.name.split(' ')[0]; } private getAgeYear() { // 私有成员方法,只有在类内部可以访问 return new Date().getFullYear() - this.age; } public getPersonInfo() { const surname = this.getSurname(); const birthYear = this.getAgeYear(); return `${surname} ${this.sex}, born in ${birthYear}.`; } } class Employee extends Person { public company: string; constructor(name: string, age: number, sex: string, company: string) { super(name, age, sex); this.company = company; } public getEmployeeInfo() { const personInfo = this.getPersonInfo(); return `This is ${personInfo}, and I work at ${this.company}.`; } } const employee1 = new Employee('Tom Smith', 30, 'male', 'ABC Company'); console.log(employee1.getEmployeeInfo()); // 输出 "This is Smith male, born in 1991., and I work at ABC Company." // console.log(employee1.age); // 报错:Property 'age' is protected and only accessible within class 'Person' and its subclasses. // console.log(employee1.sex); // 报错:Property 'sex' is private and only accessible within class 'Person'.
在上面的示例代码中,定义了一个 Person
类,并使用 public、protected、private 修饰符分别修饰了成员变量和成员方法,同时定义了一个 Employee
子类,用于继承 Person
类并增加一个公司名的成员变量。
可以看到,在 Person
类中,name
变量和 sayHello()
函数都有 public 修饰符,因此可以在任何地方被访问。age
和 getSurname()
都被 protected 修饰符限制,因此只能在 Person
类中及其子类中访问。sex
和 getAgeYear()
都被 private 修饰符限制,因此只能在 Person
类的内部访问。
在 Employee
类中,使用 super()
方法调用 Person
类的 constructor,并通过继承实现了 getPersonInfo()
方法同时增加了一个 company
成员变量和 getEmployeeInfo()
方法。
总之,在 TypeScript 中,访问修饰符可以帮助开发者更好地控制成员变量和成员方法的访问权限,从而提高代码的安全性和可维护性。
只读属性
在 TypeScript 中,可以使用 readonly
关键字定义只读属性。只读属性是指不能在运行时更改其值的属性。
只读属性可以在声明时初始化或在构造函数中初始化。如果尝试在其他地方更改其值,TypeScript 编译器将会报错。
下面是一个示例代码,演示了如何在 TypeScript 中使用只读属性:
class Person { public readonly name: string; private readonly age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log(`Hello, my name is ${this.name}, and I'm ${this.age} years old.`); } } const person1 = new Person('Tom', 30); person1.sayHello(); // 输出 "Hello, my name is Tom, and I'm 30 years old." // person1.name = 'Jerry'; // 报错:Cannot assign to 'name' because it is a read-only property. // person1.age = 31; // 报错:Cannot assign to 'age' because it is a read-only property.
在上面的示例中,Person
类使用了 readonly
修饰符来声明 name
和 age
成员变量为只读属性。在 constructor
中初始化这些成员变量后,它们的值不能在其他地方被修改。创建 Person
实例后,可以调用 sayHello()
方法,但尝试修改 name
和 age
属性的值将会报错。
总之,在 TypeScript
中,使用只读属性可以帮助开发者确保某些属性在运行时不被意外修改,从而提高代码的稳定性和可靠性。
静态成员
在 TypeScript
中,类的静态成员是指与类本身相关联,并不依赖于类的实例的成员。静态成员通常被用于共享相同数据或行为,例如用于计算工具函数或常量数据等。
在 TypeScript
中,可以使用 static
关键字来定义静态成员。静态成员可以是静态属性或静态方法,可以通过类名来访问,而不是类的实例。
下面是一个示例代码,演示如何在 TypeScript 中使用静态成员:
class Person { public name: string; private age: number; static species: string = 'Human'; // 静态属性 constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log(`Hello, my name is ${this.name}, and I'm ${this.age} years old.`); } static getSpecies() { // 静态方法 console.log(`This is a ${Person.species}.`); } } const person1 = new Person('Tom', 30); person1.sayHello(); // 输出 "Hello, my name is Tom, and I'm 30 years old." Person.getSpecies(); // 输出 "This is a Human." console.log(Person.species); // 输出 "Human" // console.log(person1.species); // 报错:Property 'species' is a static member of type 'Person'
在上面的示例中,Person
类定义了一个名为 species
的静态属性和一个名为 getSpecies()
的静态方法。通过 static
关键字定义这些静态成员后,可以通过类名来访问它们,而不需要创建类的实例。注意,在类方法中,静态属性可以通过类名访问,例如 Person.species
。
总之,在 TypeScript 中,静态成员可以用于封装与类相关的数据和行为,并通过类名访问,而不需要从类的实例中访问。它们也比实例成员更容易共享和封装,因为它们与特定实例无关。
成员函数
访问修饰符
在 TypeScript
中,类的成员函数也可以使用访问修饰符来限制其访问权限和可见性。类的成员函数访问修饰符有三种,与成员变量的访问修饰符相同:
public
:公共修饰符,被修饰的成员函数可以在类的内部、子类和类的外部访问(默认为public
,可以省略)。protected
:受保护修饰符,被修饰的成员函数只能在类的内部和子类中访问。private
:私有修饰符,被修饰的成员函数只能在类的内部访问。
下面是一个简单示例,演示了在 TypeScript 中使用不同成员函数访问修饰符的方法:
class Person { public name: string; protected age: number; private sex: string; constructor(name: string, age: number, sex: string) { this.name = name; this.age = age; this.sex = sex; } public sayHello() { // 公共成员函数 console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old, and I'm ${this.sex}.`); } protected getSurname() { // 受保护成员函数 return this.name.split(' ')[0]; } private getAgeYear() { // 私有成员函数 return new Date().getFullYear() - this.age; } } class Employee extends Person { public company: string; constructor(name: string, age: number, sex: string, company: string) { super(name, age, sex); this.company = company; } public getEmployeeInfo() { const surname = this.getSurname(); const birthYear = this.getAgeYear(); // 子类可以继承使用受保护成员函数 return `My name is ${this.name}, I was born in ${birthYear}, my surname is ${surname}, and I work for ${this.company}`; } } const employee1 = new Employee('Tom Smith', 30, 'male', 'ABC Company'); console.log(employee1.getEmployeeInfo()); // 输出 "My name is Tom Smith, I was born in 1991, my surname is Tom, and I work for ABC Company" // employee1.getAgeYear(); // 报错:属性“getAgeYear”为私有属性,只能在类“Person”中访问。
在上述示例代码中,Person
类中定义了三个成员函数,其中 sayHello()
函数使用了默认的公共访问修饰符,可以在任何地方被访问。getSurname()
函数使用了 protected
访问修饰符,表示它只能在类的内部和子类中访问。getAgeYear()
函数使用了 private
访问修饰符,表示它只能在类的内部访问。
在 Employee
子类中,可以继承使用 getSurname()
等受保护成员函数。同时,定义了公共成员函数 getEmployeeInfo()
,并在其中使用了 getSurname()
和 getAgeYear()
函数,以及基类的公共成员函数 sayHello()
访问其成员变量。因为这些函数和成员变量的访问权限均被正确限制,从而保证了代码的稳定性和可维护性。
总之,成员函数的访问修饰符可以帮助开发者更好地控制成员函数的访问范围和可见性,从而提高代码的安全性和可维护性。
参数和返回类型
在 TypeScript 中,可以使用参数和返回类型来描述类或方法的输入和输出。参数和返回类型也有助于提高代码的可读性和可维护性,并帮助 IDE 和编译器检查代码错误和类型不匹配。
在 TypeScript 中,可以为类的成员变量和成员函数指定参数和返回类型,以下是一些示例:
- 为类的成员变量指定类型:
class Person { public name: string; private age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } }
在上面的示例中,name
和 age
成员变量都被指定了类型:name
是字符串类型,age
是数字类型。
- 为类的成员函数指定参数和返回类型:
class Calculator { public add(x: number, y: number): number { return x + y; } } const calculator = new Calculator(); console.log(calculator.add(1, 2)); // 输出 3 // console.log(calculator.add('1', '2')); // 报错:Argument of type '"1"' is not assignable to parameter of type 'number'.
在上面的示例中,add
函数的参数 x
和 y
都被指定为数字类型,而返回类型被指定为数字类型,从而确保了该函数的输入和输出类型正确。
另外,可以使用泛型来增强类或方法的通用性和可重用性,例如:
class Box<T> { private contents: T; constructor(contents: T) { this.contents = contents; } public getContents(): T { return this.contents; } } const stringBox = new Box('hello'); // 通过泛型创建一个字符串盒子 console.log(stringBox.getContents().charAt(1)); // 输出 "e" const numberBox = new Box(1234); // 通过泛型创建一个数字盒子 console.log(numberBox.getContents().toFixed(2)); // 输出 "1234.00"
在以上示例代码中,Box
类使用了泛型 T
,它可以是任何类型。contents
成员变量使用了泛型 T
类型,同时构造函数和 getContents()
成员函数也都使用了泛型 T
类型,使得该类具有通用性和可重用性。
总之,在 TypeScript 中,参数和返回类型可以帮助开发者更好地描述类或方法的输入输出,并提高代码的可读性和可维护性。同时,使用泛型可以进一步增强类或方法的通用性和可重用性。
this 关键字
在 TypeScript 中,this
关键字表示当前对象或实例,在类定义中经常被使用。
通常在类中使用 this
表示当前类的实例对象,也可以在成员函数内部使用 this
来访问和操作该类的成员变量成员函数。
以下是一些示例:
class Person { public name: string; private age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } public sayHello() { console.log(`Hello, my name is ${this.name}, and I'm ${this.age} years old.`); } public changeAge() { setTimeout(function() { this.age++; // 这里的 this 指向了 window,而不是 Person 类的实例对象 console.log(`Now I'm ${this.age} years old.`); }, 1000); } } const person1 = new Person('Tom', 30); person1.sayHello(); // 输出 "Hello, my name is Tom, and I'm 30 years old." // person1.changeAge(); // 输出 "Now I'm NaN years old."
在上面的示例中,Person
类使用 this.name
和 this.age
等语句来引用该类的实例变量。例如,在 sayHello()
方法中,this.name
和 this.age
引用的是该类的实例变量 name
和 age
。但是在 changeAge()
方法中,由于 setTimeout
的回调函数是全局函数,不是该类的成员函数,因此不能直接使用 this.age
来引用该类的实例变量,需要使用变量缓存。如果不使用缓存变量,setTimeout
的回调函数中的 this
会指向 window
对象,导致结果错误。
为了避免这种问题,可以使用箭头函数或函数绑定来确保 this
指向该类的实例对象。例如:
class Person { public name: string; private age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } public changeAge() { setTimeout(() => { // 使用箭头函数确保 this 指向该类的实例对象 this.age++; console.log(`Now I'm ${this.age} years old.`); }, 1000); } } const person1 = new Person('Tom', 30); person1.changeAge(); // 输出 "Now I'm 31 years old."
在上面的代码中,changeAge()
方法使用了箭头函数来确保其回调函数中的 this
指向该类的实例对象。
总之,在 TypeScript 中,this
关键字表示当前的对象或实例,在类定义中经常被使用。在使用 this
时需要注意使用上下文环境,避免出现由于 this
指向不明导致的问题。使用箭头函数或函数绑定可以确保 this
指向当前上下文的对象或实例。
TypeScript 类的基础:从定义到实例化,让你快速掌握(二)https://developer.aliyun.com/article/1426389