TypeScript 类
TypeScript 完全支持 ES2015 引入的 class 关键字,并在此基础上添加了类型注解和其他语法,用于表达类与其他类型之间的关系。
类成员
字段(Fields)
字段声明会在类上创建一个公共的可写属性:
class Point {
x: number;
y: number;
}
const point = new Point();
point.x = 10;
point.y = 20;
console.log(`坐标: (${point.x}, ${point.y})`); // 坐标: (10, 20)
解释:x 和 y 是类的字段,类型注解是可选的。如果未指定类型,则默认为 any 类型。
字段可以有初始值,在类实例化时会自动执行:
class Point {
x = 0;
y = 0;
}
const point = new Point();
console.log(`${point.x}, ${point.y}`); // 0, 0
解释:初始值会用于推断字段类型,x 和 y 被推断为 number 类型。
readonly 修饰符
使用 readonly 修饰符可以将字段设为只读,只能在构造函数中赋值:
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
// 错误:无法分配到 "name" ,因为它是只读属性
// this.name = "not ok";
}
}
const g = new Greeter("hello");
// g.name = "also not ok"; // 错误:无法分配到 "name"
console.log(g.name); // "hello"
解释:readonly 属性只能在声明时或构造函数中初始化,之后无法修改。这有助于创建不可变对象。
构造函数(Constructors)
构造函数与函数类似,可以添加参数类型注解、默认值和重载:
class Point {
x: number;
y: number;
// 带默认值的构造函数
constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
}
const p1 = new Point(); // (0, 0)
const p2 = new Point(10); // (10, 0)
const p3 = new Point(10, 20); // (10, 20)
构造函数重载
class Point {
x: number;
y: number;
constructor(x: number, y: number);
constructor(xy: string);
constructor(xOrXy: number | string, y?: number) {
if (typeof xOrXy === "string") {
// 解析字符串格式的坐标
const parts = xOrXy.split(",");
this.x = parseFloat(parts[0]);
this.y = parseFloat(parts[1]);
} else {
this.x = xOrXy;
this.y = y ?? 0;
}
}
}
const p1 = new Point(10, 20); // 数字坐标
const p2 = new Point("10,20"); // 字符串坐标
解释:构造函数重载允许类以多种方式实例化,实现签名必须兼容所有重载签名。
super 调用
如果类继承自基类,在使用 this 之前必须调用 super():
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// 错误:在派生类的构造函数中访问 'this' 前必须调用 'super()'
// console.log(this.k);
super(); // 必须先调用 super()
console.log(this.k); // 4
}
}
解释:JavaScript 要求在派生类构造函数中,必须先完成父类构造函数的调用(super())才能访问 this。TypeScript 会检查这一点。
方法(Methods)
类中的函数属性称为方法,可以使用与函数相同的类型注解:
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
const point = new Point();
point.scale(2);
console.log(`${point.x}, ${point.y}`); // 20, 20
重要提示:在方法体内,访问字段和其他方法必须通过 this.:
let x = 10; // 外部变量
class Example {
x = "hello";
method() {
// 这是修改外部的 x,不是类的属性
// x = "world"; // 类型错误:string 不能赋给 number
// 正确的方式
this.x = "world";
}
}
存取器(Getters / Setters)
类可以有存取器(getter 和 setter):
class Circle {
private _radius: number = 0;
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value < 0) {
throw new Error("半径不能为负数");
}
this._radius = value;
}
get area(): number {
return Math.PI * this._radius ** 2;
}
}
const circle = new Circle();
circle.radius = 10;
console.log(circle.radius); // 10
console.log(circle.area); // 314.159...
// circle.area = 100; // 错误:area 只有 getter,是只读的
解释:
- 只有
get没有set的属性自动变为readonly - 如果未指定 setter 参数类型,会从 getter 的返回类型推断
从 TypeScript 4.3 开始,getter 和 setter 可以使用不同的类型:
class Thing {
private _size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
const num = Number(value);
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
const thing = new Thing();
thing.size = 100; // number
thing.size = "200"; // string
thing.size = true; // boolean (转为 1)
继承
extends 关键字
类可以继承自基类,派生类拥有基类的所有属性和方法:
class Animal {
move(): void {
console.log("正在移动...");
}
}
class Dog extends Animal {
woof(times: number): void {
for (let i = 0; i < times; i++) {
console.log("汪!");
}
}
}
const dog = new Dog();
dog.move(); // 继承自基类的方法
dog.woof(3); // 派生类自己的方法
方法重写
派生类可以重写基类的方法,使用 super. 语法访问基类方法:
class Base {
greet(): void {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string): void {
if (name === undefined) {
super.greet(); // 调用基类方法
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet(); // Hello, world!
d.greet("reader"); // Hello, READER
重要:派生类必须遵循基类的约定。重写方法时,参数类型必须兼容:
class Base {
greet(): void {
console.log("Hello!");
}
}
class Derived extends Base {
// 错误:派生类方法参数不兼容
// greet(name: string): void {
// console.log(`Hello, ${name}`);
// }
}
解释:因为可以这样调用 const b: Base = d; b.greet();,如果 greet 需要参数,就会出错。
初始化顺序
JavaScript 类的初始化顺序可能令人惊讶:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
const d = new Derived(); // 输出 "base",不是 "derived"!
解释:初始化顺序如下:
- 基类字段初始化(
name = "base") - 基类构造函数执行(输出
this.name,此时是 "base") - 派生类字段初始化(
name = "derived") - 派生类构造函数执行
所以在基类构造函数执行时,派生类的字段还未初始化。
implements 子句
使用 implements 子句检查类是否满足特定接口:
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping(): void {
console.log("ping!");
}
}
// 错误:类 'Ball' 错误实现接口 'Pingable'
// class Ball implements Pingable {
// pong(): void {
// console.log("pong!");
// }
// }
类可以实现多个接口:
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
interface AlarmInterface {
alarm(): void;
}
class Clock implements ClockInterface, AlarmInterface {
currentTime: Date = new Date();
setTime(d: Date): void {
this.currentTime = d;
}
alarm(): void {
console.log("闹钟响了!");
}
}
注意:implements 子句只是检查类是否符合接口,不会改变类的类型:
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
// 注意:参数 s 没有 string 类型!
check(s) { // s 隐式为 any 类型
return s.toLowerCase() === "ok";
}
}
解释:implements 不会自动给方法参数添加类型,需要手动添加。
成员可见性
TypeScript 提供三种访问修饰符控制成员可见性。
public(公开)
public 是默认的可见性,成员可以在任何地方访问:
class Greeter {
public greet(): void {
console.log("hi!");
}
}
const g = new Greeter();
g.greet(); // OK
public 是默认的,所以不需要显式写出来。
protected(受保护)
protected 成员只能在声明它的类及其子类中访问:
class Greeter {
public greet(): void {
console.log("Hello, " + this.getName());
}
protected getName(): string {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy(): void {
// 可以在子类中访问 protected 成员
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
// g.getName(); // 错误:属性 'getName' 受保护
派生类可以将 protected 成员暴露为 public:
class Base {
protected m = 10;
}
class Derived extends Base {
// 没有修饰符,默认是 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK,m 现在是 public
private(私有)
private 成员只能在声明它的类中访问,子类也无法访问:
class Base {
private x = 0;
}
const b = new Base();
// console.log(b.x); // 错误:属性 'x' 是私有的
class Derived extends Base {
showX() {
// 错误:属性 'x' 是私有的
// console.log(this.x);
}
}
跨实例私有访问:TypeScript 允许同一类的不同实例互相访问私有成员:
class A {
private x = 10;
public sameAs(other: A): boolean {
// 可以访问其他实例的私有成员
return other.x === this.x;
}
}
const a1 = new A();
const a2 = new A();
console.log(a1.sameAs(a2)); // true
JavaScript 私有字段(#):ES2022 引入了真正的私有字段语法:
class Dog {
#barkAmount = 0; // 真正的私有字段
personality = "happy";
bark(): void {
this.#barkAmount++;
console.log(`汪!(${this.#barkAmount} 次)`);
}
}
const dog = new Dog();
dog.bark(); // 汪!(1 次)
// dog.#barkAmount; // 语法错误
解释:# 前缀的私有字段是 JavaScript 原生的私有字段,在运行时也保持私有,而 TypeScript 的 private 只是编译时检查。
静态成员
静态成员不属于某个特定实例,而是属于类本身:
class MyClass {
static x = 0;
static printX(): void {
console.log(MyClass.x);
}
}
console.log(MyClass.x); // 0
MyClass.printX(); // 0
const instance = new MyClass();
// instance.x; // 错误:x 是静态成员
静态成员也可以使用可见性修饰符:
class MyClass {
private static x = 0;
public static getX(): number {
return MyClass.x;
}
}
// console.log(MyClass.x); // 错误:x 是私有的
console.log(MyClass.getX()); // 0
静态成员也会被继承:
class Base {
static getGreeting(): string {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting(); // 继承静态方法
}
静态代码块
静态代码块用于初始化静态成员:
class Foo {
static #count = 0;
get count(): number {
return Foo.#count;
}
static {
// 静态初始化块
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
} catch {
// 忽略错误
}
}
}
抽象类
抽象类不能被实例化,只能被继承:
abstract class Animal {
// 抽象方法:没有实现,必须由子类实现
abstract makeSound(): void;
// 具体方法:有实现,子类可以直接继承
move(): void {
console.log("正在移动...");
}
}
// const animal = new Animal(); // 错误:无法创建抽象类的实例
class Dog extends Animal {
makeSound(): void {
console.log("汪汪汪!");
}
}
const dog = new Dog();
dog.makeSound(); // 汪汪汪!
dog.move(); // 正在移动...
解释:
abstract关键字标记抽象类和抽象方法- 抽象方法必须在派生类中实现
- 抽象类可以包含具体实现的方法
泛型类
类可以有泛型参数:
class Box<T> {
contents: T;
constructor(value: T) {
this.contents = value;
}
getValue(): T {
return this.contents;
}
}
const stringBox = new Box<string>("hello");
console.log(stringBox.getValue()); // "hello"
const numberBox = new Box(123); // 类型推断为 Box<number>
console.log(numberBox.getValue()); // 123
泛型类可以使用泛型约束和默认值:
interface Item {
id: number;
}
class Collection<T extends Item> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
}
interface User extends Item {
id: number;
name: string;
}
const users = new Collection<User>();
users.add({ id: 1, name: "张三" });
users.add({ id: 2, name: "李四" });
console.log(users.findById(1)); // { id: 1, name: "张三" }
注意:静态成员不能引用类的泛型参数:
class Box<T> {
// 错误:静态成员不能引用类类型参数
// static defaultValue: T;
}
解释:因为运行时只有一个 Box.defaultValue,而 Box<string> 和 Box<number> 是不同的类型。
this 类型
在类中,特殊的 this 类型动态指向当前类的类型:
class Box {
content: string = "";
set(value: string): this {
this.content = value;
return this; // 返回 this 类型
}
}
class ClearableBox extends Box {
clear(): this {
this.content = "";
return this;
}
}
const box = new ClearableBox();
const result = box.set("hello"); // result 类型是 ClearableBox,不是 Box
result.clear(); // OK
解释:使用 this 作为返回类型,可以方便地实现链式调用,并且在继承时保持正确的类型。
this 也可以用作参数类型:
class Box {
content: string = "";
sameAs(other: this): boolean {
return other.content === this.content;
}
}
class DerivedBox extends Box {
extra: string = "";
}
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base); // OK
// base.sameAs(derived); // 错误:参数类型不匹配
this 类型守卫
可以在方法返回类型中使用 this is Type:
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
}
class FileRep extends FileSystemObject {
constructor(public path: string, public content: string) {
super();
}
}
class Directory extends FileSystemObject {
children: FileSystemObject[] = [];
}
function process(fso: FileSystemObject) {
if (fso.isFile()) {
// fso 类型被收窄为 FileRep
console.log(fso.content);
} else if (fso.isDirectory()) {
// fso 类型被收窄为 Directory
console.log(fso.children);
}
}
参数属性
TypeScript 提供了一种简写方式,将构造函数参数自动变成类属性:
class Person {
// 传统写法
// name: string;
// age: number;
// constructor(name: string, age: number) {
// this.name = name;
// this.age = age;
// }
// 参数属性简写
constructor(
public name: string,
public age: number,
private id: number,
readonly email: string
) {}
}
const person = new Person("张三", 25, 1, "[email protected]");
console.log(person.name); // 张三
console.log(person.age); // 25
console.log(person.email); // [email protected]
// console.log(person.id); // 错误:id 是私有的
解释:在构造函数参数前添加可见性修饰符(public、private、protected)或 readonly,TypeScript 会自动创建并初始化对应的属性。
小结
本章我们学习了 TypeScript 类的核心概念:
- 类成员:字段、方法、构造函数、存取器
- 继承:
extends和方法重写 - 接口实现:
implements子句 - 成员可见性:
public、protected、private - 静态成员:属于类本身的成员
- 抽象类:不能实例化的基类
- 泛型类:带类型参数的类
- this 类型:动态指向当前类类型
- 参数属性:构造函数参数的简写语法
练习
- 创建一个
Rectangle类,包含宽度和高度属性,以及计算面积和周长的方法 - 创建一个抽象类
Shape,派生出Circle和Square类 - 实现一个泛型类
Stack<T>,包含 push、pop 和 peek 方法 - 使用参数属性简化一个包含多个属性的类