TypeScript 中 class 作为类型和接口的差异。
核心概念:class 作为类型和接口
在 TypeScript 中,class 可以扮演双重角色:
- 作为类型(Type): 当
class作为类型使用时,它描述了对象的结构,包括属性和方法。类似于接口,它可以用来定义变量、函数参数、返回值的类型。 - 作为实现(Implementation): 当
class作为实现使用时,它不仅定义了对象的结构,还提供了具体的属性值和方法实现。
class 作为类型:与 interface 的对比
主要区别在于 interface 仅仅定义了类型规范,不提供具体实现,而 class 在定义类型的同时,也提供了实现。
| 特性 | interface | class 作为类型 |
|---|---|---|
| 实现 | 仅定义类型规范,不提供实现 | 定义类型规范,同时也提供实现 |
| 可继承性 | 可以 extends 多个 interface | 可以 extends 一个 class (或 null) |
| 可实现性 | 类可以 implements 多个 interface | 类可以 extends 一个 class |
| 构造函数 | 不能定义构造函数 | 可以定义构造函数 |
| 成员访问修饰符 | 不能定义访问修饰符(如 public,private,protected) | 可以定义访问修饰符 |
| 实例创建 | 不可创建实例 | 可以创建实例 |
| 灵活性 | 更侧重抽象和类型约束 | 在类型约束的同时,具备实现逻辑,可以减少重复代码 |
代码示例:
为了更好理解这些差异,我们将通过多个代码示例进行对比,并详细解释每个部分。
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()方法。Circle和Square类通过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,定义了点的x和y属性,以及distanceToOrigin方法。calculateDistance函数接受两个Point类型的参数。- 这里的
Point被用作类型,表示calculateDistance函数的参数必须具有x和y属性,并且可以调用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关键字继承其他class。Dog继承自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可以使用public,private,和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作为类型时,除了类型约束外,还可以提供具体的实现、构造函数、访问修饰符,以及继承等特性, 更加灵活。- 在实际项目中,通常
interface与class结合使用。interface用于定义类型的契约,class用于实现和构建对象。 - 抽象类
abstract class可以提供类型定义的同时提供通用方法,并可以强制子类实现抽象方法。
选择的建议:
- 纯粹的类型约束: 如果只是想定义类型,不关心具体的实现,使用
interface。 - 需要构造函数、访问修饰符、继承等: 使用
class。 - 需要类型定义和通用实现,强制子类实现特定方法: 使用
abstract class