类
类(Class)是一种用于创建对象的蓝图或模板。类定义了一组属性(properties)和方法(methods),这些属性和方法被封装在一个单一的单元中,用于表示现实世界中的实体或概念。
类的基本结构
一个类由以下几个部分组成:
- 类名:用于标识类的名称,通常是名词或名词短语。
- 属性(也称为字段):用于存储与类相关的数据。属性可以是任何基本数据类型(如数字、字符串等)或引用类型(如数组、对象等)。
- 方法:定义了类可以执行的操作或行为。方法通常与类的实例相关联,并可以访问和修改类的属性。
- 构造函数(Constructor):一个特殊的方法,用于在创建类的新实例时初始化对象。构造函数通常具有与类名相同的名称,并且没有返回类型(隐式地返回类的实例)。
下面是一个TypeScript类的简单示例:
class Person {
// 属性
name: string;
age: number;
// 构造函数
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// 方法
greet(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
// 创建Person类的一个实例
let person1 = new Person("Alice", 30);
// 调用实例上的方法
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
类的特性
- 封装:通过将属性和方法组织在单个类中,并将它们视为一个独立的单元,可以实现数据的封装和隐藏。类的外部只能通过类提供的方法来访问或修改其内部状态。
- 继承:TypeScript支持类的继承,允许你基于一个已存在的类来定义一个新的类(子类)。子类继承父类的属性和方法,并可以添加新的属性、方法或重写继承的方法。
- 多态:多态允许你以统一的接口处理不同类型的对象。在TypeScript中,这通常通过接口和类的组合来实现。
- 访问修饰符:TypeScript支持访问修饰符(如public、private和protected),用于控制类成员的可见性和可访问性。
类的继承示例
现在,我们创建一个Student类,它继承自Person类,并添加了一个新属性(studentId)和一个新方法(study)。
class Student extends Person {
studentId: number;
// 调用父类的构造函数
constructor(name: string, age: number, studentId: number) {
super(name, age); // 调用父类的constructor(name: string, age: number)
this.studentId = studentId;
}
// 重写父类的方法(可选)
// 这里我们没有重写greet方法,但我们可以这样做
// 添加新方法
study(): void {
console.log(`${this.name} is studying with student ID ${this.studentId}.`);
}
}
// 实例化Student类
let student1 = new Student("Bob", 20, 12345);
// 访问继承的属性
console.log(student1.name); // 输出: Bob
console.log(student1.age); // 输出: 20
// 访问自己的属性
console.log(student1.studentId); // 输出: 12345
// 调用继承的方法
student1.greet(); // 输出: Hello, my name is Bob and I am 20 years old.
// 调用自己的方法
student1.study(); // 输出: Bob is studying with student ID 12345.
在这个示例中,Student类通过extends关键字继承了Person类。在Student类的构造函数中,我们使用super关键字来调用父类的构造函数,并传递相应的参数。然后,我们添加了Student类特有的属性studentId和新方法study。派生类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 一定要调用 super()。
通过类的继承,我们可以重用Person类中的代码,并在Student类中添加或修改特定的行为,从而实现了代码的复用和扩展。
公共,私有与受保护的修饰符
Public(公开的)
- public 修饰符允许类成员在类的外部被访问。
- 如果没有指定访问修饰符,则默认为 public。
class MyClass {
public myPublicProperty: string = 'I am public';
public myPublicMethod(): void {
console.log('This is a public method.');
}
}
let instance = new MyClass();
console.log(instance.myPublicProperty); // 可以访问
instance.myPublicMethod(); // 可以调用
Private(私有的)
- private 修饰符允许类成员只能在类的内部被访问,而不能在类的外部被访问。
- 这有助于封装类的内部实现细节,防止外部代码直接访问这些成员。
class MyClass {
private myPrivateProperty: string = 'I am private';
public myMethod(): void {
console.log(this.myPrivateProperty); // 在类内部可以访问
}
}
let instance = new MyClass();
console.log(instance.myPrivateProperty); // 错误:'myPrivateProperty' 是私有的
instance.myMethod(); // 正确,可以在方法内部访问
Protected(受保护的)
- protected 修饰符允许类成员在类的内部被访问,同时也允许子类访问这些成员,但不允许类的外部访问。
- 这提供了一种在类层次结构中共享数据和方法的方式,同时保持对外部世界的封装。
class BaseClass {
protected myProtectedProperty: string = 'I am protected';
protected myProtectedMethod(): void {
console.log('This is a protected method.');
}
}
class SubClass extends BaseClass {
public accessProtectedMember(): void {
console.log(this.myProtectedProperty); // 在子类中可以访问
this.myProtectedMethod(); // 在子类中可以调用
}
}
let baseInstance = new BaseClass();
console.log(baseInstance.myProtectedProperty); // 错误:'myProtectedProperty' 是受保护的
baseInstance.myProtectedMethod(); // 错误:'myProtectedMethod' 是受保护的
let subInstance = new SubClass();
subInstance.accessProtectedMember(); // 正确,通过子类的方法访问
在实际开发中,根据成员的使用场景和访问需求,选择合适的访问修饰符是非常重要的。它有助于保持代码的封装性、可维护性和可扩展性。
存取器
存取器(Accessors)与在 JavaScript 中的工作方式非常相似,但 TypeScript 提供了类型系统和更严格的编译时检查。存取器允许你获取(getter)和设置(setter)对象的属性,同时执行一些额外的逻辑,如验证、日志记录等。
在 TypeScript 中,你可以在类中使用 get 和 set 关键字来定义存取器。
Getter
Getter 允许你定义一个方法来获取私有或受保护属性的值,但让外部代码看起来像是在直接访问属性。
class Person {
private _firstName: string;
private _lastName: string;
constructor(firstName: string, lastName: string) {
this._firstName = firstName;
this._lastName = lastName;
}
// Getter
get fullName(): string {
return `${this._firstName} ${this._lastName}`;
}
}
const person = new Person("John", "Doe");
console.log(person.fullName); // 输出: John Doe
Setter
Setter 允许你定义一个方法来设置私有或受保护属性的值,但让外部代码看起来像是在直接设置属性。Setter 可以接受一个参数(即你想要设置的值),并允许你执行一些额外的逻辑。
class Person {
private _firstName: string;
private _lastName: string;
constructor(firstName: string, lastName: string) {
this._firstName = firstName;
this._lastName = lastName;
}
// Setter
set fullName(value: string) {
const parts = value.split(" ");
if (parts.length !== 2) {
throw new Error("Full name must have a first and last name.");
}
this._firstName = parts[0];
this._lastName = parts[1];
}
// Getter
get fullName(): string {
return `${this._firstName} ${this._lastName}`;
}
}
const person = new Person("John", "Doe");
person.fullName = "Jane Smith"; // 正确设置
console.log(person.fullName); // 输出: Jane Smith
// person.fullName = "Invalid Name"; // 这将抛出错误
在上面的例子中,fullName 属性有一个 getter 和一个 setter。 getter 返回 _firstName 和 _lastName 的组合,而 setter 接收一个字符串,将其拆分为两部分,并验证是否包含两个单词。如果验证失败(例如,如果只有一个单词或超过两个单词),则 setter 抛出一个错误。
注意点
- 当你定义存取器时,通常会在属性名前加一个下划线(如 _firstName ),以表明这是一个私有属性,只能通过类的公共方法来访问。
- TypeScript 不会阻止你直接访问私有属性(例如,_firstName ),但这是一个约定俗成的做法,以保持代码的清晰性和可维护性。
- 存取器允许你在对象属性的获取和设置过程中添加额外的逻辑,这是它们在面向对象编程中非常有用的原因之一。
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get不带有 set的存取器自动被推断为 readonly。 这在从代码生成 .d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
抽象类
- 以 abstract开头的类是抽象类
- 抽象类和其他类区别不大,只是不能用来创建对象(对类的定义)
- 抽象类就是专门用来被继承的类
- 抽象类中添可以添加抽象方法,抽象方法只能定义抽象类中,子类必须对抽象方法进行重写
// 定义一个抽象类
abstract class Animal {
// 属性
name: string;
age: number;
// 构造函数,对象创立之前调用
constructor(name: string, age: number) {
// this表示当前实例
this.name = name;
this.age = age;
}
// 抽象方法 没有方法体
// 抽象方法只能定义抽象类中,子类必须对抽象方法进行重写
abstract makeSound(): void;
// 非抽象方法
move(): void {
console.log('The animal can move.');
}
}
// 定义一个Dog类继承自Animal类
class Dog extends Animal {
run() {
console.log(this.name + "快跑");
}
// 必须实现继承自Animal的抽象方法 重写
makeSound(): void {
console.log('Woof!');
}
}
// 由于Animal是抽象类,所以不能直接实例化
// let myAnimal = new Animal(); // 这将引发编译错误
// 可以实例化Dog类,因为Dog类实现了Animal类的所有抽象方法
let myDog = new Dog("修奥黑", 20);
myDog.run();// 输出: 修奥黑快跑
myDog.makeSound(); // 输出: Woof!
myDog.move(); // 输出: The animal can move.
在这个例子中,Animal是一个抽象类,它有一个抽象方法makeSound()和一个非抽象方法move()。Dog类继承了Animal类,并实现了makeSound()方法。然后,我们创建了一个Dog的实例,并通过这个实例调用了makeSound()和move()方法。
抽象类的特点
- 不能被实例化:你不能直接通过new关键字来创建抽象类的实例。
- 可以包含抽象方法:抽象类中可以包含抽象方法,这些抽象方法没有具体的实现,必须在继承它的子类中实现。
- 可以包含非抽象方法:抽象类也可以包含实现了的方法,这些方法可以在抽象类中直接定义。
- 作为基类:抽象类通常被用作其他类的基类,为子类提供一个公共的接口或框架。
注意事项
- 如果一个类包含至少一个抽象方法,那么这个类也必须被声明为抽象类。
- 抽象类不能被实例化,即你不能使用new关键字来创建抽象类的实例。
- 抽象类中的抽象方法不需要在抽象类中实现,但是必须在继承它的非抽象子类中实现。
- 抽象类可以作为其他类的基类,为子类提供一个共同的接口或框架。