MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript类的定义与基本使用

2024-03-225.8k 阅读

TypeScript 类的定义基础

在 TypeScript 中,类是一种面向对象编程的基础结构,它用于封装数据和行为。类的定义由关键字 class 开始,后面跟着类名。例如:

class Person {
}

上述代码定义了一个名为 Person 的类。此时,这个类还没有任何属性和方法,但它已经是一个有效的类定义。

类的属性定义

类的属性是类所包含的数据成员。在 TypeScript 中,我们可以在类中直接声明属性,并指定其类型。例如:

class Person {
    name: string;
    age: number;
}

这里定义了 Person 类的两个属性 nameage,分别为字符串类型和数字类型。属性在使用前必须声明,这有助于在编译时捕获类型错误。

我们还可以在声明属性时给它一个初始值:

class Person {
    name: string = 'John';
    age: number = 30;
}

这样,当创建 Person 类的实例时,name 会初始化为 Johnage 会初始化为 30

类的构造函数

构造函数是类中的一个特殊方法,它在创建类的实例时被调用。在 TypeScript 中,构造函数使用 constructor 关键字定义。例如:

class Person {
    name: string;
    age: number;
    constructor(n: string, a: number) {
        this.name = n;
        this.age = a;
    }
}

在上述代码中,Person 类的构造函数接受两个参数 na,并将它们分别赋值给类的 nameage 属性。this 关键字在这里用于引用类的实例本身。

通过构造函数,我们可以在创建实例时传入不同的值来初始化对象。例如:

let person1 = new Person('Jane', 25);
console.log(person1.name); // 输出: Jane
console.log(person1.age);  // 输出: 25

访问修饰符

TypeScript 提供了三种访问修饰符:publicprivateprotected,用于控制类的属性和方法的访问权限。

  1. public(默认):被 public 修饰的属性和方法可以在类的内部和外部被访问。例如:
class Person {
    public name: string;
    public constructor(n: string) {
        this.name = n;
    }
    public greet() {
        return `Hello, I'm ${this.name}`;
    }
}
let person = new Person('Bob');
console.log(person.name);  // 输出: Bob
console.log(person.greet()); // 输出: Hello, I'm Bob
  1. private:被 private 修饰的属性和方法只能在类的内部被访问。如果在类的外部尝试访问 private 成员,会导致编译错误。例如:
class Person {
    private name: string;
    constructor(n: string) {
        this.name = n;
    }
    private greet() {
        return `Hello, I'm ${this.name}`;
    }
    public getGreeting() {
        return this.greet();
    }
}
let person = new Person('Alice');
// console.log(person.name);  // 编译错误
// console.log(person.greet()); // 编译错误
console.log(person.getGreeting()); // 输出: Hello, I'm Alice

这里 name 属性和 greet 方法是 private 的,不能在类外部直接访问,但可以通过类内部的 public 方法 getGreeting 间接访问。

  1. protectedprotected 修饰符与 private 类似,不同之处在于 protected 成员可以在类本身及其子类中被访问。例如:
class Animal {
    protected name: string;
    constructor(n: string) {
        this.name = n;
    }
    protected speak() {
        return `${this.name} makes a sound`;
    }
}
class Dog extends Animal {
    bark() {
        return this.speak() + 'and barks';
    }
}
let dog = new Dog('Buddy');
// console.log(dog.name);  // 编译错误
// console.log(dog.speak()); // 编译错误
console.log(dog.bark()); // 输出: Buddy makes a sound and barks

在这个例子中,Dog 类继承自 Animal 类,它可以访问 Animal 类中 protectedname 属性和 speak 方法。

类的方法定义

类的方法是类中定义的函数,它可以操作类的属性,实现特定的行为。方法的定义与普通函数类似,只是它在类的内部。

实例方法

实例方法是与类的实例相关联的方法。它们可以访问和修改实例的属性。例如:

class Counter {
    value: number;
    constructor() {
        this.value = 0;
    }
    increment() {
        this.value++;
    }
    decrement() {
        if (this.value > 0) {
            this.value--;
        }
    }
    getValue() {
        return this.value;
    }
}
let counter = new Counter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 输出: 2
counter.decrement();
console.log(counter.getValue()); // 输出: 1

在上述 Counter 类中,incrementdecrementgetValue 都是实例方法。它们可以访问和修改 Counter 实例的 value 属性。

静态方法

静态方法是与类本身相关联的方法,而不是与类的实例相关联。静态方法使用 static 关键字定义,它们不能直接访问实例属性,只能访问静态属性。例如:

class MathUtils {
    static add(a: number, b: number): number {
        return a + b;
    }
    static multiply(a: number, b: number): number {
        return a * b;
    }
}
let result1 = MathUtils.add(3, 5);
let result2 = MathUtils.multiply(4, 6);
console.log(result1); // 输出: 8
console.log(result2); // 输出: 24

在这个例子中,addmultiplyMathUtils 类的静态方法。我们通过类名直接调用这些方法,而不需要创建类的实例。

类的继承

继承是面向对象编程中的一个重要概念,它允许一个类从另一个类中获取属性和方法。在 TypeScript 中,使用 extends 关键字来实现继承。

基本继承

例如,我们有一个 Animal 类,然后创建一个 Dog 类继承自 Animal 类:

class Animal {
    name: string;
    constructor(n: string) {
        this.name = n;
    }
    speak() {
        return `${this.name} makes a sound`;
    }
}
class Dog extends Animal {
    breed: string;
    constructor(n: string, b: string) {
        super(n);
        this.breed = b;
    }
    bark() {
        return `${this.speak()} and barks`;
    }
}
let dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.speak()); // 输出: Buddy makes a sound
console.log(dog.bark()); // 输出: Buddy makes a sound and barks

在上述代码中,Dog 类继承了 Animal 类的 name 属性和 speak 方法。Dog 类还定义了自己的 breed 属性和 bark 方法。在 Dog 类的构造函数中,使用 super 关键字调用了 Animal 类的构造函数,以初始化继承自 Animal 类的 name 属性。

重写方法

子类可以重写从父类继承的方法。当子类重写方法时,方法的签名(参数列表和返回类型)必须与父类中的方法签名相同(或兼容)。例如:

class Animal {
    speak() {
        return 'Animal makes a sound';
    }
}
class Cat extends Animal {
    speak() {
        return 'Meow';
    }
}
let animal = new Animal();
let cat = new Cat();
console.log(animal.speak()); // 输出: Animal makes a sound
console.log(cat.speak()); // 输出: Meow

这里 Cat 类重写了 Animal 类的 speak 方法,提供了自己的实现。

访问修饰符与继承

继承过程中,访问修饰符会影响子类对父类成员的访问。public 成员在子类中可以自由访问和重写;protected 成员可以在子类中访问和重写;private 成员在子类中不可访问。例如:

class Parent {
    public publicMethod() {
        return 'Public method';
    }
    protected protectedMethod() {
        return 'Protected method';
    }
    private privateMethod() {
        return 'Private method';
    }
}
class Child extends Parent {
    callProtected() {
        return this.protectedMethod();
    }
    // callPrivate() { // 编译错误
    //     return this.privateMethod();
    // }
}
let child = new Child();
console.log(child.publicMethod()); // 输出: Public method
console.log(child.callProtected()); // 输出: Protected method
// console.log(child.callPrivate()); // 编译错误

在这个例子中,Child 类可以访问和调用 Parent 类的 publicprotected 方法,但不能访问 private 方法。

抽象类

抽象类是一种不能被实例化的类,它主要用于作为其他类的基类。抽象类可以包含抽象方法,抽象方法是没有实现体的方法,必须在子类中被实现。

定义抽象类和抽象方法

使用 abstract 关键字来定义抽象类和抽象方法。例如:

abstract class Shape {
    abstract getArea(): number;
    abstract getPerimeter(): number;
}
class Circle extends Shape {
    radius: number;
    constructor(r: number) {
        super();
        this.radius = r;
    }
    getArea(): number {
        return Math.PI * this.radius * this.radius;
    }
    getPerimeter(): number {
        return 2 * Math.PI * this.radius;
    }
}
class Rectangle extends Shape {
    width: number;
    height: number;
    constructor(w: number, h: number) {
        super();
        this.width = w;
        this.height = h;
    }
    getArea(): number {
        return this.width * this.height;
    }
    getPerimeter(): number {
        return 2 * (this.width + this.height);
    }
}
// let shape = new Shape(); // 编译错误,不能实例化抽象类
let circle = new Circle(5);
console.log(circle.getArea()); // 输出: 78.53981633974483
console.log(circle.getPerimeter()); // 输出: 31.41592653589793
let rectangle = new Rectangle(4, 6);
console.log(rectangle.getArea()); // 输出: 24
console.log(rectangle.getPerimeter()); // 输出: 20

在上述代码中,Shape 类是一个抽象类,它定义了两个抽象方法 getAreagetPerimeterCircleRectangle 类继承自 Shape 类,并实现了这些抽象方法。

抽象类的作用

抽象类为一组相关的类提供了一个通用的接口和部分实现。它强制子类遵循一定的契约,即必须实现抽象类中定义的抽象方法。这有助于提高代码的可维护性和可扩展性,特别是在大型项目中,不同的开发者可以基于抽象类来实现具体的功能,同时保持代码结构的一致性。

类与接口的关系

接口和类在 TypeScript 中都用于定义类型结构,但它们有着不同的用途和特点。

类实现接口

一个类可以实现一个或多个接口,以表明它提供了接口所定义的行为。使用 implements 关键字来实现接口。例如:

interface Printable {
    print(): void;
}
class Book implements Printable {
    title: string;
    constructor(t: string) {
        this.title = t;
    }
    print() {
        console.log(`Book title: ${this.title}`);
    }
}
let book = new Book('TypeScript in Action');
book.print(); // 输出: Book title: TypeScript in Action

在这个例子中,Book 类实现了 Printable 接口,这意味着 Book 类必须提供 print 方法的实现。

接口继承接口

接口之间也可以继承,通过继承可以扩展接口的功能。例如:

interface Shape {
    getArea(): number;
}
interface RectangleShape extends Shape {
    getPerimeter(): number;
}
class Rectangle implements RectangleShape {
    width: number;
    height: number;
    constructor(w: number, h: number) {
        this.width = w;
        this.height = h;
    }
    getArea(): number {
        return this.width * this.height;
    }
    getPerimeter(): number {
        return 2 * (this.width + this.height);
    }
}
let rectangle = new Rectangle(4, 6);
console.log(rectangle.getArea()); // 输出: 24
console.log(rectangle.getPerimeter()); // 输出: 20

这里 RectangleShape 接口继承自 Shape 接口,并添加了 getPerimeter 方法。Rectangle 类实现 RectangleShape 接口时,需要实现 Shape 接口的 getArea 方法和 RectangleShape 接口新增的 getPerimeter 方法。

类与接口的区别

  1. 实例化:类可以被实例化,创建具体的对象;而接口不能被实例化,它只是一种类型定义。
  2. 实现与继承:类通过 extends 关键字继承另一个类,通过 implements 关键字实现接口;接口通过 extends 关键字继承其他接口。
  3. 成员定义:类可以包含属性、方法、构造函数等各种成员,并且可以使用访问修饰符控制访问权限;接口主要定义方法签名和属性类型,不包含具体的实现代码,也没有访问修饰符。

类的高级特性

只读属性

在 TypeScript 中,可以使用 readonly 关键字将属性声明为只读。只读属性只能在声明时或构造函数中赋值。例如:

class Person {
    readonly name: string;
    constructor(n: string) {
        this.name = n;
    }
}
let person = new Person('Tom');
// person.name = 'Jerry'; // 编译错误,不能修改只读属性

这里 name 属性被声明为只读,在构造函数中赋值后,就不能再修改。

参数属性

参数属性是一种在构造函数参数中声明并初始化属性的便捷方式。例如:

class Person {
    constructor(public name: string, public age: number) {
    }
}
let person = new Person('Alice', 28);
console.log(person.name); // 输出: Alice
console.log(person.age);  // 输出: 28

在上述代码中,nameage 属性通过构造函数参数直接声明并初始化,public 访问修饰符同时定义了属性的访问权限。

存取器(Getters 和 Setters)

存取器允许我们控制对类属性的访问。get 关键字用于定义读取属性值的方法,set 关键字用于定义设置属性值的方法。例如:

class Temperature {
    private _value: number;
    constructor(value: number) {
        this._value = value;
    }
    get value() {
        return this._value;
    }
    set value(newValue: number) {
        if (newValue >= -273.15) {
            this._value = newValue;
        } else {
            throw new Error('Temperature cannot be below absolute zero');
        }
    }
}
let temp = new Temperature(25);
console.log(temp.value); // 输出: 25
temp.value = 30;
console.log(temp.value); // 输出: 30
// temp.value = -274; // 抛出错误: Temperature cannot be below absolute zero

在这个例子中,value 属性通过 getset 存取器进行访问控制。set 方法在设置值之前进行了有效性检查。

多态

多态是指同一个方法在不同的类中有不同的实现。通过继承和方法重写,TypeScript 实现了多态。例如:

class Animal {
    speak() {
        return 'Animal makes a sound';
    }
}
class Dog extends Animal {
    speak() {
        return 'Woof';
    }
}
class Cat extends Animal {
    speak() {
        return 'Meow';
    }
}
function makeSound(animal: Animal) {
    console.log(animal.speak());
}
let dog = new Dog();
let cat = new Cat();
makeSound(dog); // 输出: Woof
makeSound(cat); // 输出: Meow

在上述代码中,makeSound 函数接受一个 Animal 类型的参数,当传入不同子类(DogCat)的实例时,会调用相应子类重写的 speak 方法,这就是多态的体现。

使用类进行模块化开发

在实际项目中,通常会将类组织成模块,以提高代码的可维护性和复用性。

模块的定义

在 TypeScript 中,可以使用 export 关键字将类导出为模块。例如,创建一个 person.ts 文件:

export class Person {
    name: string;
    age: number;
    constructor(n: string, a: number) {
        this.name = n;
        this.age = a;
    }
    greet() {
        return `Hello, I'm ${this.name} and I'm ${this.age} years old`;
    }
}

在另一个文件 main.ts 中,可以导入并使用这个类:

import { Person } from './person';
let person = new Person('Bob', 35);
console.log(person.greet()); // 输出: Hello, I'm Bob and I'm 35 years old

这里使用 import 语句从 person.ts 文件中导入了 Person 类。

模块的组织

可以在一个模块中导出多个类,也可以将相关的类组织在不同的文件中,通过模块进行管理。例如,创建一个 animal.ts 文件:

export class Animal {
    name: string;
    constructor(n: string) {
        this.name = n;
    }
    speak() {
        return `${this.name} makes a sound`;
    }
}
export class Dog extends Animal {
    bark() {
        return `${this.speak()} and barks`;
    }
}

main.ts 文件中导入并使用这些类:

import { Animal, Dog } from './animal';
let animal = new Animal('Generic Animal');
console.log(animal.speak()); // 输出: Generic Animal makes a sound
let dog = new Dog('Buddy');
console.log(dog.speak()); // 输出: Buddy makes a sound
console.log(dog.bark()); // 输出: Buddy makes a sound and barks

通过合理地组织模块和类,可以使项目结构更加清晰,代码更易于维护和扩展。

总结类在前端开发中的应用

在前端开发中,TypeScript 的类提供了强大的面向对象编程能力。通过类,我们可以更好地组织和管理代码,封装数据和行为,实现代码的复用和扩展。

在构建复杂的用户界面时,类可以用于表示视图组件,将组件的状态和行为封装在类中,通过继承和多态实现组件的复用和定制。例如,我们可以定义一个基础的 UIComponent 类,然后让 ButtonInput 等具体的 UI 组件类继承自它,实现统一的样式和交互逻辑。

同时,类与接口的结合使用,可以更好地定义组件之间的契约和交互方式,提高代码的可维护性和可测试性。在处理业务逻辑时,类可以用于表示业务实体和业务规则,通过合理地设计类的属性和方法,实现业务逻辑的清晰表达和高效执行。

总之,熟练掌握 TypeScript 类的定义与使用,对于提升前端开发的质量和效率具有重要意义。无论是小型项目还是大型企业级应用,类都能在代码的组织和架构中发挥关键作用。