TypeScript 中 `class` 作为类型和接口的差异

163 阅读4分钟

TypeScript 中 class 作为类型和接口的差异。

核心概念:class 作为类型和接口

在 TypeScript 中,class 可以扮演双重角色:

  1. 作为类型(Type):class 作为类型使用时,它描述了对象的结构,包括属性和方法。类似于接口,它可以用来定义变量、函数参数、返回值的类型。
  2. 作为实现(Implementation):class 作为实现使用时,它不仅定义了对象的结构,还提供了具体的属性值和方法实现。

class 作为类型:与 interface 的对比

主要区别在于 interface 仅仅定义了类型规范,不提供具体实现,而 class 在定义类型的同时,也提供了实现。

特性interfaceclass 作为类型
实现仅定义类型规范,不提供实现定义类型规范,同时也提供实现
可继承性可以 extends 多个 interface可以 extends 一个 class (或 null)
可实现性类可以 implements 多个 interface类可以 extends 一个 class
构造函数不能定义构造函数可以定义构造函数
成员访问修饰符不能定义访问修饰符(如 publicprivateprotected可以定义访问修饰符
实例创建不可创建实例可以创建实例
灵活性更侧重抽象和类型约束在类型约束的同时,具备实现逻辑,可以减少重复代码

代码示例:

为了更好理解这些差异,我们将通过多个代码示例进行对比,并详细解释每个部分。

1. interface 仅仅定义类型:

interface Shape {
    color: string;
    area(): number;
}

class Circle implements Shape {
    color: string;
    radius: number;

    constructor(color: string, radius: number) {
        this.color = color;
        this.radius = radius;
    }

    area(): number {
        return Math.PI * this.radius * this.radius;
    }
}

class Square implements Shape {
    color: string;
    side: number;

    constructor(color: string, side: number) {
        this.color = color;
        this.side = side;
    }

    area(): number {
        return this.side * this.side;
    }
}


function printShapeArea(shape: Shape) {
    console.log(`The area of the shape is ${shape.area()}`);
}

const circle = new Circle("red", 5);
const square = new Square("blue", 10);


printShapeArea(circle); // 输出: The area of the shape is 78.53981633974483
printShapeArea(square);  // 输出: The area of the shape is 100

// Shape 是一个 interface 类型
let myShape: Shape;
myShape = circle; // 正确
myShape = square; // 正确

解释:

  • Shape 是一个 interface,定义了形状必须有的 color 属性和 area() 方法。
  • CircleSquare 类通过 implements Shape 来声明它们符合 Shape 的类型规范,它们必须提供 color 属性和 area() 方法。
  • printShapeArea 函数接受一个 Shape 类型的参数,这意味着它可以接受任何实现了 Shape 接口的对象。
  • 可以看到, interface 仅仅定义了类型规范,并没有提供实现细节,类的实现需要各自完成。

2. class 作为类型:


class Point {
    x: number;
    y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }

    distanceToOrigin(): number {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    }

}


function calculateDistance(pointA: Point, pointB: Point): number {
    const dx = pointA.x - pointB.x;
    const dy = pointA.y - pointB.y;
    return Math.sqrt(dx * dx + dy * dy)
}


const point1 = new Point(3,4);
const point2 = new Point(0,0);

console.log(calculateDistance(point1,point2)); // 输出: 5
console.log(point1.distanceToOrigin()); // 输出: 5

// Point 是一个 class 类型
let myPoint: Point;
myPoint = point1; // 正确
myPoint = point2; // 正确

解释:

  • Point 是一个 class,定义了点的 xy 属性,以及 distanceToOrigin 方法。
  • calculateDistance 函数接受两个 Point 类型的参数。
  • 这里的 Point 被用作类型,表示 calculateDistance 函数的参数必须具有 xy 属性,并且可以调用 distanceToOrigin 方法。
  • interface 不同,class 提供构造函数和方法实现。

3. class 的继承性 vs interface 的多继承:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    makeSound() : void {
        console.log("Generic Animal Sound");
    }
}

class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }

    makeSound(): void {
        console.log("Woof!");
    }

    bark() {
      console.log("Bark Bark!");
    }
}

class Cat extends Animal {
    constructor(name: string) {
        super(name);
    }
    makeSound() : void {
      console.log("Meow!");
    }

    purr() {
      console.log("Purrr");
    }
}


function animalSound(animal: Animal) {
  animal.makeSound()
}

const dog = new Dog("Buddy", "Golden Retriever");
const cat = new Cat("Whiskers");

animalSound(dog); // 输出: Woof!
animalSound(cat); // 输出: Meow!


// interface 的多继承
interface CanFly {
    fly(): void;
}

interface CanSwim {
    swim(): void;
}

interface Bird extends CanFly {
    layEggs() : void;
}

class Duck implements Bird,CanSwim {
  layEggs(): void {
    console.log("Egg laid")
  }

  fly(): void {
      console.log("Flap!");
  }

  swim(): void {
    console.log("Splash!");
  }
}


const duck = new Duck();
duck.fly()  // 输出: Flap!
duck.swim()  // 输出: Splash!
duck.layEggs() // 输出: Egg laid

let myAnimal: Animal;
myAnimal = dog;  // 正确
myAnimal = cat;  // 正确
// myAnimal = duck; // Error: 类型不匹配,Duck不是Animal

解释:

  • class 可以使用 extends 关键字继承其他 classDog 继承自 Animal 并扩展了自己的属性和方法。
  • interface 可以使用 extends 关键字继承其他 interface,实现类型的多继承 Bird extends CanFly
  • 类可以使用 implements 实现多个 interface, Duck implements Bird, CanSwim
  • 一个类只能 extends 一个类,但可以 implements 多个接口。

4. class 构造函数和访问修饰符:

class Car {
    public make: string;
    private model: string;
    protected year: number;

    constructor(make: string, model: string, year: number) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    public getModel(): string {
        return this.model;
    }
    protected getYear(): number {
      return this.year;
    }

    public printDetails() {
        console.log(`${this.make} - ${this.model} - ${this.year}`);
    }
}


class ElectricCar extends Car {
    batteryCapacity: number;

    constructor(make: string, model: string, year: number, batteryCapacity: number) {
        super(make, model, year);
        this.batteryCapacity = batteryCapacity;
    }

    public printElectricCarDetails(){
      this.printDetails() // 可以访问父类的 public 方法
      console.log(`Battery: ${this.batteryCapacity}`)
      console.log(`Year: ${this.getYear()}`);  // 可以访问父类的 protected 方法
      // console.log(this.model) // Error: 不能访问父类的 private 属性
    }
}

const myCar = new Car("Toyota", "Camry", 2022);
myCar.printDetails();  // 输出:Toyota - Camry - 2022
console.log(myCar.getModel());  // 输出: Camry
// console.log(myCar.year)  // Error: 不能访问受保护的属性
// console.log(myCar.model)  // Error: 不能访问私有的属性


const myElectricCar = new ElectricCar("Tesla", "Model 3", 2023, 75);
myElectricCar.printElectricCarDetails();
// 输出:
// Toyota - Camry - 2022
// Battery: 75
// Year: 2023


// interface 不支持构造函数和访问修饰符
// interface PointInterface {
//     constructor(x: number, y: number) // Error: 接口不能有构造函数
//     private x: number // Error: 接口不能有访问修饰符
//     y: number;
// }

解释:

  • class 可以定义构造函数 (constructor) 来初始化实例的属性。
  • class 可以使用 publicprivate,和 protected 修饰符来控制成员的访问权限。
  • public: 成员可以被任何地方访问。
  • private: 成员只能在当前类的内部访问。
  • protected: 成员可以被当前类以及子类访问。
  • interface 不支持构造函数和访问修饰符。

5. class 作为抽象类

abstract class ShapeAbstract {
    abstract color: string;
    abstract area(): number;

    abstract getPerimeter() : number;

    printDetails(){
      console.log(`Shape color: ${this.color}, area: ${this.area()}, Perimeter: ${this.getPerimeter()}`)
    }
}

class CircleAbstract extends ShapeAbstract {
    color: string;
    radius: number;

    constructor(color: string, radius: number) {
        super();
        this.color = color;
        this.radius = radius;
    }

    area(): number {
        return Math.PI * this.radius * this.radius;
    }

    getPerimeter(): number {
        return 2 * Math.PI * this.radius
    }
}

class SquareAbstract extends ShapeAbstract {
    color: string;
    side: number;

    constructor(color: string, side: number) {
        super()
        this.color = color;
        this.side = side;
    }

    area(): number {
        return this.side * this.side;
    }

    getPerimeter() : number {
      return this.side * 4;
    }
}

//  let abstractShape = new ShapeAbstract(); // Error: 无法创建抽象类的实例

const circleAbstract = new CircleAbstract("green", 8);
const squareAbstract = new SquareAbstract("yellow", 4);

circleAbstract.printDetails() // 输出: Shape color: green, area: 201.06192982974676, Perimeter: 50.26548245743669
squareAbstract.printDetails() // 输出: Shape color: yellow, area: 16, Perimeter: 16

function processShapeAbstract(shape : ShapeAbstract){
  shape.printDetails()
}

processShapeAbstract(circleAbstract); // 输出: Shape color: green, area: 201.06192982974676, Perimeter: 50.26548245743669
processShapeAbstract(squareAbstract);  // 输出: Shape color: yellow, area: 16, Perimeter: 16

解释:

  • 抽象类 abstract class 可以定义抽象方法,不提供具体实现,要求子类必须实现这些抽象方法。
  • abstract 关键字可以用来修饰类本身和类成员。
  • 抽象类本身不能被实例化,只能通过继承实现。
  • 抽象类可以有具体的方法实现,提供默认的行为。
  • 抽象类更倾向于作为类型定义,并且提供通用的方法实现,而 interface 只能提供类型约束。

总结

  • interface 主要用于定义类型规范,侧重于类型约束和抽象,不提供具体实现。
  • class 作为类型时,除了类型约束外,还可以提供具体的实现、构造函数、访问修饰符,以及继承等特性, 更加灵活。
  • 在实际项目中,通常 interfaceclass 结合使用。 interface 用于定义类型的契约,class 用于实现和构建对象。
  • 抽象类 abstract class 可以提供类型定义的同时提供通用方法,并可以强制子类实现抽象方法。

选择的建议:

  • 纯粹的类型约束: 如果只是想定义类型,不关心具体的实现,使用 interface
  • 需要构造函数、访问修饰符、继承等: 使用 class
  • 需要类型定义和通用实现,强制子类实现特定方法: 使用 abstract class