📚 TypeScript 教程系列
- 入门与配置
- 基础类型与变量声明
- 函数
- 流程控制与运算符
- 集合类型
- 异步编程与错误处理
- 接口与类(本文)
- 泛型与类型组合
- 高级类型
- 模块、装饰器与工程化
⚠️ 来源声明:本文内容参考自 菜鸟教程 TypeScript 教程,仅供学习交流,版权归原作者所有。
TypeScript 的接口与类是面向对象编程的核心构件:接口负责描述对象的形状与契约,类负责封装数据与行为并支持继承与多态。本篇将接口、类、访问修饰符、抽象类、继承与混入串联起来,形成一条从"类型契约"到"代码复用"的完整脉络。
接口
接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。需要注意接口不会转换为 JavaScript,它只是 TypeScript 编译时的一部分。
TypeScript 接口定义如下:
1 2
| interface interface_name { }
|
接口属性与方法
以下实例中,定义了一个接口 IPerson,接着定义了一个变量 customer,它的类型是 IPerson,customer 实现了接口的属性和方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| interface IPerson { firstName: string, lastName: string, sayHi: () => string }
var customer: IPerson = { firstName: "Tom", lastName: "Hanks", sayHi: (): string => { return "Hi there" } }
console.log("Customer 对象 ") console.log(customer.firstName) console.log(customer.lastName) console.log(customer.sayHi())
var employee: IPerson = { firstName: "Jim", lastName: "Blakes", sayHi: (): string => { return "Hello!!!" } }
console.log("Employee 对象 ") console.log(employee.firstName) console.log(employee.lastName)
|
输出结果为:
1 2 3 4 5 6 7
| Customer 对象 Tom Hanks Hi there Employee 对象 Jim Blakes
|
可选属性
接口里的属性不全都是必需的。带有可选属性的接口与普通接口类似,只是在属性名后面加一个 ?,表示该属性可以不存在。这对于"条件性"的对象描述非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| interface Person { name: string; age?: number; phone?: string; }
var p1: Person = { name: "Alice" };
var p2: Person = { name: "Bob", age: 20, phone: "13800000000" };
console.log(p1.name); console.log(p2.name + ", " + p2.age);
|
只读属性
一些对象属性只能在对象刚刚创建时修改其值,使用 readonly 指定只读属性。只读属性只能在声明时或构造函数里赋值,之后不可修改。
1 2 3 4 5 6 7 8 9 10
| interface Point { readonly x: number; readonly y: number; }
var p: Point = { x: 10, y: 20 };
console.log(p.x + ", " + p.y);
|
readonly 与 const 的区别:const 用于变量,readonly 用于属性。若希望属性不可变,使用 readonly;若希望变量名绑定的引用不可变,使用 const。
索引签名
接口可以描述"任意数量的属性"的对象,通过索引签名实现。索引值可以是数字或字符串。
设置元素为字符串类型:
1 2 3 4 5 6 7 8 9
| interface namelist { [index: number]: string }
var list2: namelist = ["Google", "Runoob", "Taobao"]
|
如果使用了其他类型会报错:
1 2 3 4 5 6
| interface namelist { [index: number]: string }
var list2: namelist = ["John", 1, "Bran"]
|
执行后报错如下,显示类型不一致:
1 2 3 4 5 6 7 8 9 10 11 12
| test.ts:8:30 - error TS2322: Type 'number' is not assignable to type 'string'.
8 var list2:namelist = ["John",1,"Bran"] ~
test.ts:2:4 2 [index:number]:string ~~~~~~~~~~~~~~~~~~~~~ The expected type comes from this index signature.
Found 1 error.
|
字符串索引签名则允许以任意字符串作为键:
1 2 3 4 5 6 7 8 9 10 11
| interface ages { [index: string]: number }
var agelist: ages;
agelist["runoob"] = 15
|
函数类型接口
接口除了描述带属性的对象,也可以描述函数类型。为函数类型定义接口时,需要给接口定义一个调用签名,就像是一个只有参数列表和返回值类型的函数定义。
1 2 3 4 5 6 7 8 9 10 11 12
| interface SearchFunc { (source: string, subString: string): boolean; }
var mySearch: SearchFunc; mySearch = function (src: string, sub: string): boolean { var result = src.search(sub); return result > -1; }
console.log(mySearch("Hello World", "World")); console.log(mySearch("Hello World", "world"));
|
函数的参数名不需要与接口里定义的名字一致,只要对应位置上的类型兼容即可。
联合类型与接口
以下实例演示了如何在接口中使用联合类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| interface RunOptions { program: string; commandline: string[] | string | (() => string); }
var options: RunOptions = { program: "test1", commandline: "Hello" }; console.log(options.commandline)
options = { program: "test1", commandline: ["Hello", "World"] }; console.log(options.commandline[0]); console.log(options.commandline[1]);
options = { program: "test1", commandline: () => { return "**Hello World**"; } }; var fn: any = options.commandline; console.log(fn());
|
输出结果为:
1 2 3 4
| Hello Hello World **Hello World**
|
接口与数组
接口中可以将数组的索引值和元素设置为不同类型,索引值可以是数字或字符串。数字索引签名常用于约束数组元素类型,字符串索引签名常用于约束字典对象。
1 2 3 4 5 6
| interface namelist { [index: number]: string }
var list2: namelist = ["Google", "Runoob", "Taobao"]
|
接口继承
接口继承就是说接口可以通过其他接口来扩展自己。TypeScript 允许接口继承多个接口,继承使用关键字 extends。
单接口继承语法格式:
1
| Child_interface_name extends super_interface_name
|
多接口继承语法格式:
1
| Child_interface_name extends super_interface1_name, super_interface2_name, ..., super_interfaceN_name
|
继承的各个接口使用逗号 , 分隔。
单继承实例
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface Person { age: number }
interface Musician extends Person { instrument: string }
var drummer = <Musician>{}; drummer.age = 27 drummer.instrument = "Drums" console.log("年龄: " + drummer.age) console.log("喜欢的乐器: " + drummer.instrument)
|
输出结果为:
多继承实例
1 2 3 4 5 6 7 8 9 10 11 12
| interface IParent1 { v1: number }
interface IParent2 { v2: number }
interface Child extends IParent1, IParent2 { }
var Iobj: Child = { v1: 12, v2: 23 } console.log("value 1: " + Iobj.v1 + " value 2: " + Iobj.v2)
|
输出结果为:
类实现接口
类可以使用 implements 关键字实现接口,确保类符合接口定义的契约。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| interface ILoan { interest: number; }
class AgriLoan implements ILoan { interest: number; rebate: number;
constructor(interest: number, rebate: number) { this.interest = interest; this.rebate = rebate; } }
var loan = new AgriLoan(10, 1); console.log("利率: " + loan.interest + "%,回扣: " + loan.rebate);
|
输出结果为:
一个类也可以同时实现多个接口,接口之间用逗号分隔。
鸭子类型(Duck Typing)
TypeScript 的类型兼容性基于"结构子类型"(Structural Subtyping),也就是常说的"鸭子类型":如果一个对象具有接口所要求的全部属性,并且类型兼容,那么它就被认为实现了该接口,无论它是否显式声明。换句话说,“如果它走起来像鸭子、叫起来像鸭子,那么它就是鸭子”。
1 2 3 4 5 6 7 8 9 10 11
| interface Point { x: number; y: number; }
var p = { x: 10, y: 20, z: 30 };
var point: Point = p; console.log(point.x + ", " + point.y);
|
鸭子类型让 TypeScript 在保持类型安全的同时具备良好的灵活性:只要结构匹配就允许赋值,而不强制要求显式的继承或实现关系。
类
类是面向对象编程(OOP)的核心概念,它是一种模板或蓝图,用于创建具有相同属性和方法的对象。TypeScript 完全支持面向对象编程,提供了类、继承、访问修饰符等特性。类封装了数据(属性)和行为(方法),使得代码更加模块化、可复用和易维护。通过类,我们可以创建多个具有相同结构的对象,这些对象称为类的实例。
TypeScript 使用 class 关键字定义类。一个类可以包含以下成员:
- 字段(Field):类中声明的变量,表示对象的属性
- 构造函数(Constructor):类实例化时调用的特殊方法,用于初始化对象
- 方法(Method):类中定义的函数,表示对象的行为
语法格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class class_name { field1: type; field2: type;
constructor(parameters) { }
methodName(): return_type { } }
|
字段与构造函数
类的字段是存储对象数据的地方,构造函数在对象创建时自动调用,用于初始化字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Car { engine: string;
constructor(engine: string) { this.engine = engine; }
disp(): void { console.log("发动机型号: " + this.engine); } }
var car = new Car("V8 发动机");
console.log("读取发动机: " + car.engine);
car.disp();
|
运行结果:
1 2
| 读取发动机: V8 发动机 发动机型号: V8 发动机
|
说明:
this 关键字指向当前类的实例- 构造函数的参数名可以与字段名相同,通过
this.field 区分 - 使用
new 关键字创建类的实例
方法
方法是类中定义的函数,描述对象的行为。方法可以访问类的字段,也可以调用其它方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Calculator { value: number;
constructor(initial: number) { this.value = initial; }
add(n: number): void { this.value += n; }
current(): number { return this.value; } }
var calc = new Calculator(10); calc.add(5); console.log("当前值: " + calc.current());
|
运行结果:
存取器(Getter / Setter)
TypeScript 支持通过 get 和 set 关键字定义存取器,把对属性的访问包装成方法调用,从而在读写时加入校验逻辑。使用存取器时,通常把真正的存储字段设为私有。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Employee { private _fullName: string = "";
get fullName(): string { return this._fullName; }
set fullName(newName: string) { if (newName && newName.length > 0) { this._fullName = newName; } else { console.log("名字不能为空"); } } }
var emp = new Employee(); emp.fullName = "Alice"; console.log(emp.fullName);
emp.fullName = ""; console.log(emp.fullName);
|
运行结果:
只带有 get 不带有 set 的存取器会被自动推断为 readonly。
静态成员
使用 static 关键字定义的成员属于类本身,而不是类的实例。可以直接通过类名访问,不需要创建实例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class StaticMem { static num: number;
static disp(): void { console.log("num 值为 " + StaticMem.num); } }
StaticMem.num = 12; StaticMem.disp();
|
运行结果:
静态成员常用于定义类的常量、工具方法或单例模式。在静态方法内部只能访问静态成员,不能直接访问实例成员,也不能使用 this 指向实例。
instanceof 运算符
instanceof 用于判断对象是否是某个类的实例。
1 2 3 4 5 6 7
| class Person { }
var obj = new Person(); var isPerson = obj instanceof Person;
console.log("obj 是 Person 类的实例吗? " + isPerson);
|
运行结果:
1
| obj 是 Person 类的实例吗? true
|
访问修饰符
访问修饰符是 TypeScript 面向对象编程的核心特性之一,用于控制类成员(属性、方法、构造函数)的可见性。通过访问修饰符,可以实现封装,保护类的内部实现细节。
面向对象编程的三大原则之一是封装。封装意味着将数据和操作数据的方法隐藏起来,对外只暴露必要的接口。访问修饰符就是实现封装的手段,它控制类成员的可见范围。TypeScript 提供三种访问修饰符:public、protected、private,以及一个相关修饰符 readonly。
修饰符作用域
| 修饰符 | 类内部 | 子类 | 类外部 |
|---|
| public | 可以访问 | 可以访问 | 可以访问 |
| protected | 可以访问 | 可以访问 | 不能访问 |
| private | 可以访问 | 不能访问 | 不能访问 |
使用场景:
- public — 公开方法/属性,如用户姓名、登录方法
- protected — 子类可见方法,如计算公式、模板方法
- private — 内部实现细节,如数据库连接、内部计算
public 修饰符
public 是默认的访问修饰符,表示成员可以在任何地方被访问。无论是类内部、子类还是类的外部,都可以访问 public 成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Animal { public name: string;
public constructor(name: string) { this.name = name; }
public speak(): void { console.log(this.name + " 发出声音"); } }
var animal = new Animal("动物");
console.log(animal.name);
animal.speak();
|
运行结果:
如果不写访问修饰符,TypeScript 会默认使用 public,因此 public 可以省略,但为了代码清晰,建议明确声明。
private 修饰符
private 表示私有成员,只能在定义它的类内部访问。子类和类的外部都不能访问 private 成员。这常用于隐藏类的内部实现细节,保护数据不被意外修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class BankAccount { private balance: number;
constructor(initialBalance: number) { this.balance = initialBalance; }
public deposit(amount: number): void { if (amount > 0) { this.balance += amount; console.log("存款成功,当前余额: " + this.balance); } }
public getBalance(): number { return this.balance; } }
var account = new BankAccount(1000);
account.deposit(500);
console.log("余额: " + account.getBalance());
|
运行结果:
1 2
| 存款成功,当前余额: 1500 余额: 1500
|
将类的内部状态设为 private,通过 public 方法提供受控的访问途径,这是实现封装的标准做法。
protected 修饰符
protected 表示受保护成员,可以在类内部和子类中访问。类的外部不能直接访问 protected 成员。这在需要让子类继承父类的某些功能,同时隐藏实现细节时非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Person { protected name: string;
constructor(name: string) { this.name = name; } }
class Employee extends Person { private department: string;
constructor(name: string, department: string) { super(name); this.department = department; }
public introduce(): string { return "我是 " + this.name + ",在 " + this.department + " 工作"; } }
var emp = new Employee("Alice", "技术部");
console.log(emp.introduce());
|
运行结果:
protected 常用于定义子类需要使用的属性,但不应该暴露给外部的值。
readonly 修饰符
readonly 用于将属性设置为只读。只能在声明时或构造函数中赋值,之后不能修改。这在定义常量或标识符时非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class User { readonly id: number; readonly name: string;
constructor(id: number, name: string) { this.id = id; this.name = name; } }
var user = new User(1, "Alice");
console.log("用户: " + user.id + ", " + user.name);
|
运行结果:
readonly 和 private 不冲突,可以同时使用 readonly 和 private,既不能修改也不能从外部访问。readonly 也可以与 public、protected 组合使用。
参数属性
TypeScript 提供了参数属性(Parameter Properties)的简写语法。可以在构造函数的参数上直接使用访问修饰符,自动创建并初始化属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Point { constructor( public x: number, public y: number, private z: number ) { }
public sum(): number { return this.x + this.y + this.z; } }
var point = new Point(1, 2, 3);
console.log("x: " + point.x); console.log("y: " + point.y);
console.log("总和: " + point.sum());
|
运行结果:
参数属性可以大幅简化类的定义,避免重复的声明和赋值代码。
访问修饰符使用建议
- 默认修饰符:不写修饰符时默认为
public - 构造函数:构造函数也可以使用访问修饰符,控制实例化权限
- readonly 组合:
readonly 可以与 public、protected、private 组合使用 - 编译影响:访问修饰符仅在编译时检查,运行时无效
最佳实践是尽可能使用最严格的访问修饰符,只暴露必要的公开接口,将内部实现设为 private 或 protected。默认使用 private,需要子类访问时使用 protected,需要完全公开时使用 public。
抽象类
抽象类是 TypeScript 面向对象编程中的重要概念。它是一种不能被直接实例化的类,只能作为基类供其他类继承。抽象类主要用于定义子类的公共属性和方法,为子类提供一个统一的结构和行为模板。
抽象类是一种介于普通类和接口之间的类型。它既有接口的特征(定义方法签名),又有类的特征(可以有具体的方法实现和构造函数)。使用 abstract 关键字声明抽象类和抽象方法。
抽象类的特性:
abstract 方法:子类必须实现- 具象方法:子类可以直接使用
- 可作为类型:接收子类实例
抽象类基础
抽象类可以包含抽象方法和具象方法(已有实现的方法)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
abstract class Animal { name: string;
constructor(name: string) { this.name = name; }
abstract speak(): void;
move(): void { console.log(this.name + " 在移动"); } }
class Dog extends Animal { speak(): void { console.log(this.name + " 汪汪汪!"); } }
var dog = new Dog("旺财"); dog.speak(); dog.move();
|
运行结果:
抽象方法没有方法体(只有方法签名),子类必须实现该方法;具象方法有具体实现,子类可以直接继承使用。
抽象方法
抽象方法是抽象类中只声明但不实现的方法。子类继承抽象类后,必须实现所有抽象方法,否则会报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| abstract class Shape { abstract area(): number; abstract perimeter(): number;
describe(): void { console.log("面积: " + this.area().toFixed(2)); } }
class Rectangle extends Shape { width: number; height: number;
constructor(width: number, height: number) { super(); this.width = width; this.height = height; }
area(): number { return this.width * this.height; }
perimeter(): number { return 2 * (this.width + this.height); } }
var rect = new Rectangle(4, 5);
rect.describe(); console.log("周长: " + rect.perimeter());
|
运行结果:
具象方法可以调用抽象方法。这是因为当具象方法被调用时,子类已经实现了抽象方法,所以可以正常执行。这正是"模板方法模式"的体现:抽象类定义骨架,子类提供具体实现。
抽象类作为类型
抽象类可以作为参数类型使用。这意味着函数可以接受任何抽象类的子类实例,实现了多态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| abstract class Animal { abstract speak(): void; }
class Cat extends Animal { speak(): void { console.log("喵喵喵!"); } }
class Dog extends Animal { speak(): void { console.log("汪汪汪!"); } }
function makeSpeak(animal: Animal): void { animal.speak(); }
makeSpeak(new Cat()); makeSpeak(new Dog());
var animals: Animal[] = [new Cat(), new Dog()];
|
运行结果:
抽象类作为类型时,实际执行的是子类的方法实现,这就是面向对象的多态特性。
抽象类与接口的区别
抽象类和接口都用于定义类型规范,但有一些关键区别。理解它们的差异有助于在实际开发中做出正确的选择。
| 特性 | 抽象类 | 接口 |
|---|
| 实例化 | 不能直接实例化 | 不能直接实例化 |
| 方法实现 | 可以有具体实现 | 只能有声明(TypeScript 3.6+ 可有默认实现) |
| 成员修饰符 | 可以添加 public、protected、private | 只能有 readonly |
| 继承 | 单继承(只能 extends 一个类) | 多实现(可以 implements 多个接口) |
| 构造函数 | 可以有构造函数 | 不能有构造函数 |
选择建议:需要共享代码使用时用抽象类;需要定义规范/契约时用接口;需要多继承时用接口。当多个类需要共享代码和逻辑时,使用抽象类;当只需要定义规范和契约时,使用接口。
完整示例:支付系统
下面是一个使用抽象类实现的支付系统示例,展示了抽象类在实际项目中的应用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| abstract class Payment { abstract process(amount: number): boolean;
validate(): void { console.log("验证支付信息"); } }
class CreditCardPayment extends Payment { cardNumber: string;
constructor(cardNumber: string) { super(); this.cardNumber = cardNumber; }
process(amount: number): boolean { console.log("处理信用卡支付: " + amount); return true; } }
class PayPalPayment extends Payment { email: string;
constructor(email: string) { super(); this.email = email; }
process(amount: number): boolean { console.log("处理 PayPal 支付: " + amount); return true; } }
var payments: Payment[] = [ new CreditCardPayment("1234"), new PayPalPayment("test@example.com") ];
for (var _i = 0, payments_1 = payments; _i < payments_1.length; _i++) { var payment = payments_1[_i]; payment.validate(); payment.process(100); }
|
运行结果:
1 2 3 4
| 验证支付信息 处理信用卡支付: 100 验证支付信息 处理 PayPal 支付: 100
|
抽象类常用于框架设计和业务逻辑分层,例如支付处理、用户认证、数据持久化等场景。
抽象类注意事项
- 不能实例化:抽象类不能直接使用
new 创建实例,必须通过子类继承 - 必须实现抽象方法:子类如果不实现所有抽象方法,会报错
- 可以有构造函数:抽象类可以定义构造函数,子类需要调用
super() - 单继承:一个类只能继承一个抽象类(单继承)
- 访问修饰符:抽象方法可以使用
public、protected 修饰符
在设计类层次结构时,优先考虑使用抽象类来共享代码和定义规范。
类继承与多态
继承允许创建一个类(子类)从另一个类(父类)获取属性和方法。子类可以复用父类的代码,还可以扩展或重写父类的行为。多态则让同一接口在不同子类上表现出不同的行为。
类的继承
使用 extends 关键字实现继承。子类会获得父类所有 public 和 protected 成员。
基本语法:
1 2 3
| class child_class extends parent_class { }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Animal { name: string;
constructor(name: string) { this.name = name; }
speak(): void { console.log(this.name + " 发出声音"); } }
class Dog extends Animal { breed: string;
constructor(name: string, breed: string) { super(name); this.breed = breed; }
speak(): void { console.log(this.name + " 汪汪汪!"); } }
var dog = new Dog("旺财", "金毛"); dog.speak();
|
运行结果:
super 关键字
super 用于调用父类的方法和构造函数。当子类定义了自己的构造函数时,必须在使用 this 之前调用 super(),以完成父类的初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Shape { color: string;
constructor(color: string) { this.color = color; }
describe(): string { return "这是一个 " + this.color + " 的图形"; } }
class Circle extends Shape { radius: number;
constructor(color: string, radius: number) { super(color); this.radius = radius; }
describe(): string { return super.describe() + ",半径是 " + this.radius; }
area(): number { return Math.PI * this.radius * this.radius; } }
var circle = new Circle("红色", 5); console.log(circle.describe()); console.log("面积: " + circle.area().toFixed(2));
|
运行结果:
1 2
| 这是一个 红色 的图形,半径是 5 面积: 78.54
|
方法重写(Override)
子类可以重写(Override)父类的方法,即在子类中定义与父类同名的方法,实现自己的行为。使用 super 关键字可以在重写的方法中调用父类的同名方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class PrinterClass { doPrint(): void { console.log("父类的 doPrint() 方法"); } }
class StringPrinter extends PrinterClass { doPrint(): void { super.doPrint();
console.log("子类的 doPrint() 方法"); } }
var obj = new StringPrinter(); obj.doPrint();
|
运行结果:
1 2
| 父类的 doPrint() 方法 子类的 doPrint() 方法
|
多态
多态是指子类的实例可以赋值给父类类型,调用同一方法时由实际对象的类型决定执行哪个实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Animal { name: string; constructor(name: string) { this.name = name; } speak(): void { console.log(this.name + " 发出声音"); } }
class Cat extends Animal { speak(): void { console.log(this.name + " 喵喵喵!"); } }
class Dog extends Animal { speak(): void { console.log(this.name + " 汪汪汪!"); } }
var animals: Animal[] = [ new Cat("小白"), new Dog("旺财"), new Animal("动物") ];
for (var _i = 0, animals_1 = animals; _i < animals_1.length; _i++) { var animal = animals_1[_i]; animal.speak(); }
|
运行结果:
instanceof 检查
使用 instanceof 可以在运行时检查实例的类型,从而在多态场景下做差异化处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Rectangle { width: number; height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } area(): number { return this.width * this.height; } }
class Circle { radius: number; constructor(radius: number) { this.radius = radius; } area(): number { return Math.PI * this.radius ** 2; } }
var shapes = [new Rectangle(4, 5), new Circle(3)];
for (var _i = 0, shapes_1 = shapes; _i < shapes_1.length; _i++) { var shape = shapes_1[_i]; if (shape instanceof Rectangle) { console.log("矩形面积: " + shape.area()); } else if (shape instanceof Circle) { console.log("圆形面积: " + shape.area().toFixed(2)); } }
|
运行结果:
多重继承与多层继承
TypeScript 不支持多继承(一个类继承多个类),但支持多层继承(A 继承 B,B 继承 C)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Root { str: string; }
class Child extends Root { }
class Leaf extends Child { }
var leaf = new Leaf(); leaf.str = "hello"; console.log("str 值: " + leaf.str);
|
运行结果:
protected 成员在继承中的可见性
protected 成员在子类中可见,但类外部不可见,这让它成为继承场景下共享内部状态的常用选择。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Person { protected name: string;
constructor(name: string) { this.name = name; } }
class Employee extends Person { private department: string;
constructor(name: string, department: string) { super(name); this.department = department; }
public introduce(): string { return "我是 " + this.name + ",在 " + this.department + " 工作"; } }
var emp = new Employee("Alice", "技术部"); console.log(emp.introduce());
|
运行结果:
继承与多态要点
- 继承:
extends 关键字实现 - super:用于调用父类构造函数与方法
- 多态:同一接口在不同子类上呈现不同实现
- protected:子类可见,外部不可见
混入(Mixin)
混入(Mixin)是一种代码复用模式,用于将多个独立的功能模块混入到一个类中。TypeScript 通过泛型函数返回扩展类的方式实现 Mixin,弥补了单继承无法复用多个来源行为的不足。
基本概念
Mixin 的核心是一个接收基类、返回扩展类的泛型函数。其中 Constructor 类型约束用于描述"任意可被继承的类"。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) { return class extends Base { createdAt = new Date(); }; }
class User { constructor(public name: string) {} }
const TimestampedUser = Timestamped(User); const user = new TimestampedUser("Alice");
console.log(user.name); console.log(user.createdAt instanceof Date);
|
运行结果:
注意:Mixin 不修改原始类,而是返回一个全新的扩展类,原始的 User 类不受影响。
组合多个 Mixin
将多个 Mixin 函数嵌套调用,即可将多份能力依次叠加到同一个类上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) { return class extends Base { createdAt = new Date(); }; }
function Serializable<TBase extends Constructor>(Base: TBase) { return class extends Base { serialize(): string { return JSON.stringify(this); } }; }
function Loggable<TBase extends Constructor<{ serialize(): string }>>(Base: TBase) { return class extends Base { log(): void { console.log("[LOG]", this.serialize()); } }; }
class Product { constructor(public name: string, public price: number) {} }
const AdvancedProduct = Loggable(Serializable(Timestamped(Product)));
const p = new AdvancedProduct("Phone", 999); p.log(); console.log(p.createdAt instanceof Date);
|
运行结果:
1 2
| [LOG] {"name":"Phone","price":999,"createdAt":"2026-..."} true
|
Mixin 与接口结合
Mixin 返回的类可以声明实现某个接口,使消费方通过接口类型操作混入后的对象,而不依赖具体实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| type Constructor<T = {}> = new (...args: any[]) => T;
interface ISerializable { serialize(): string; }
interface ICloneable<T> { clone(): T; }
function Serializable<TBase extends Constructor>(Base: TBase) { return class extends Base implements ISerializable { serialize(): string { return JSON.stringify(this); } }; }
function Cloneable<TBase extends Constructor>(Base: TBase) { return class extends Base { clone() { return Object.assign( Object.create(Object.getPrototypeOf(this)), this ); } }; }
class Article { constructor(public title: string, public content: string) {} }
const RichArticle = Cloneable(Serializable(Article));
const a1 = new RichArticle("TypeScript 入门", "正文内容..."); const a2 = a1.clone(); a2.title = "TypeScript 进阶";
console.log(a1.serialize());
console.log(a2.serialize());
console.log(a1.title === a2.title);
|
运行结果:
1 2 3
| {"title":"TypeScript 入门","content":"正文内容..."} {"title":"TypeScript 进阶","content":"正文内容..."} false
|
带约束的 Mixin
通过泛型约束,可以限定 Mixin 只能混入满足特定条件的类,避免运行时缺少必要属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| type Constructor<T = {}> = new (...args: any[]) => T;
type WithIdAndName = Constructor<{ id: number; name: string }>;
function Printable<TBase extends WithIdAndName>(Base: TBase) { return class extends Base { print(): void { console.log(`[${this.id}] ${this.name}`); } }; }
class Item { constructor(public id: number, public name: string) {} }
const PrintableItem = Printable(Item); const item = new PrintableItem(42, "Keyboard"); item.print();
|
运行结果:
Mixin 与继承的对比
| 维度 | 继承(extends) | Mixin |
|---|
| 来源数量 | 只能继承一个父类 | 可叠加任意数量 |
| 耦合程度 | 子类与父类强耦合 | 每个 Mixin 独立,低耦合 |
| 复用粒度 | 复用整个类的能力 | 按需复用单一功能 |
| 类型安全 | 原生支持 | 需借助泛型约束保证 |
| 适用场景 | 强 “is-a” 关系 | 横切关注点(日志、序列化、缓存等) |
混入要点
- 核心模式:Mixin 是接收基类、返回扩展类的泛型函数,
Constructor<T> 是标准约束类型 - 能力叠加:嵌套调用多个 Mixin 函数即可将多份能力组合到同一个类
- 接口结合:Mixin 返回的类可以实现接口,消费方只需依赖接口而非具体类
- 泛型约束:通过约束
TBase 可以限定 Mixin 的适用范围,在编译期阻止错误使用 - 适用场景:日志、序列化、克隆、时间戳等横切关注点,优于多层继承
小结
接口与类共同构成了 TypeScript 面向对象编程的两条主线:接口负责声明形状与契约,强调"结构兼容"的鸭子类型;类负责封装数据与行为,并通过访问修饰符、抽象类、继承与多态提供从内部封装到外部复用的完整能力。当单继承无法覆盖日志、序列化等横切关注点时,混入(Mixin)以泛型函数的形式叠加独立能力,是继承之外更灵活的复用手段。合理地组合这些特性,可以让代码既保持类型安全,又具备良好的可扩展性与可维护性。