73 阅读9分钟

类(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()方法。

抽象类的特点

  1. 不能被实例化:你不能直接通过new关键字来创建抽象类的实例。
  2. 可以包含抽象方法:抽象类中可以包含抽象方法,这些抽象方法没有具体的实现,必须在继承它的子类中实现。
  3. 可以包含非抽象方法:抽象类也可以包含实现了的方法,这些方法可以在抽象类中直接定义。
  4. 作为基类:抽象类通常被用作其他类的基类,为子类提供一个公共的接口或框架。

注意事项

  • 如果一个类包含至少一个抽象方法,那么这个类也必须被声明为抽象类。
  • 抽象类不能被实例化,即你不能使用new关键字来创建抽象类的实例。
  • 抽象类中的抽象方法不需要在抽象类中实现,但是必须在继承它的非抽象子类中实现。
  • 抽象类可以作为其他类的基类,为子类提供一个共同的接口或框架。