TypeScript 类
TypeScript 完全支持 ES6 的 class,并在此基础上引入了更多强大的静态类型功能。
1. TypeScript 的增强功能
访问修饰符
TypeScript 提供了三个访问修饰符来控制类成员(属性和方法)的可见性,这增强了类的封装性。
public(默认): 成员在任何地方都可以被访问。protected: 成员只能在类自身及其子类中被访问。private: 成员只能在类自身中被访问,即使是子类也无法访问。
class Person {
public name: string; // 可在任何地方访问
protected age: number; // 可在 Person 和其子类中访问
private ssn: string; // 只能在 Person 中访问
constructor(name: string, age: number, ssn: string) {
this.name = name;
this.age = age;
this.ssn = ssn;
}
}
class Employee extends Person {
constructor(name: string, age: number, ssn: string) {
super(name, age, ssn);
console.log(this.name); // OK
console.log(this.age); // OK
// console.log(this.ssn); // Error: Property 'ssn' is private...
}
}
private 与 #private 的区别
需要特别区分 TypeScript 的 private 和 ECMAScript 标准的私有字段 (#):
private: 是一个编译时的检查。它在编译后的 JavaScript 代码中会被擦除,并不能阻止在运行时通过变通方法访问该属性。#(私有字段): 是一个运行时的强制私有。编译后的代码会使用WeakMap等机制来确保该字段在外部绝对不可访问,提供了真正的封装。
class MyClass {
private tsPrivate: string = "ts";
#jsPrivate: string = "js";
checkAccess() {
console.log(this.tsPrivate); // OK
console.log(this.#jsPrivate); // OK
}
}
const instance = new MyClass();
console.log(instance.tsPrivate); // 编译时报错,但运行时可以访问 (instance['tsPrivate'])
// console.log(instance.#jsPrivate); // 编译时和运行时都会直接报错
最佳实践: 如果目标运行环境支持(ES2022+),优先使用
#实现真正的运行时私有。
只读修饰符 (readonly)
readonly 关键字确保属性只能在声明时或构造函数中被初始化,之后便不可修改。
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(theName: string) {
this.name = theName;
}
setName() {
// this.name = "New Name"; // Error: Cannot assign to 'name' because it is a read-only property.
}
}
参数属性
为了简化类的初始化代码,TypeScript 允许在构造函数参数上直接使用修饰符(public, private, protected, readonly)。这会自动创建同名属性并完成赋值。
// 冗长的写法
class Point_Long {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 使用参数属性的简洁写法
class Point_Short {
constructor(
public readonly x: number,
public readonly y: number,
) {}
}
const p = new Point_Short(10, 20);
console.log(p.x); // 10
2. 抽象类
抽象类是作为其他类的基类而存在的,它们不能被直接实例化。抽象类可以包含抽象成员(方法或属性),这些成员没有具体实现,必须在派生类中被实现。
abstract class Shape {
// 抽象属性
abstract name: string;
// 抽象方法
abstract getArea(): number;
// 普通方法
printName() {
console.log("Shape is: " + this.name);
}
}
class Circle extends Shape {
name = "Circle";
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
// 必须实现父类的抽象方法
getArea() {
return Math.PI * this.radius ** 2;
}
}
// const s = new Shape(); // Error: Cannot create an instance of an abstract class.
const c = new Circle(5);
console.log(c.getArea());
3. 类与接口
实现接口
implements 关键字用于声明一个类遵循某个接口的“契约”。它只检查类的实例部分是否满足接口的形状,但不提供任何实现。
extends用于继承实现,一个类只能继承一个父类;implements用于确保形状,一个类可以实现多个接口。
interface Serializable {
serialize(): string;
}
interface Loggable {
log(message: string): void;
}
class User implements Serializable, Loggable {
constructor(public name: string) {}
serialize() {
return JSON.stringify({ name: this.name });
}
log(message: string) {
console.log(`[User: ${this.name}] ${message}`);
}
}
类作为类型使用
当一个类被声明时,TypeScript 会同时创建两个东西:
- 一个值:即类的构造函数。
- 一个类型:代表该类实例的形状。
class Car {
/* ... */
}
let carInstance: Car = new Car(); // `Car` 在这里被用作类型
let CarFactory: typeof Car = Car; // `typeof Car` 获取构造函数的类型
6. 高级主题
类中的 this 问题
在 JavaScript 中,this 的指向在函数被调用时才确定。当类的方法作为回调函数传递时,this 的上下文会丢失,导致错误。
class MyComponent {
message = "Hello";
logMessage() {
console.log(this.message); // `this` 期望是 MyComponent 实例
}
}
const comp = new MyComponent();
const logger = comp.logMessage;
// logger(); // 运行时错误: Cannot read properties of undefined (reading 'message')
解决方案:
-
箭头函数属性: 箭头函数会捕获其定义时的
this上下文。class MyComponent { message = "Hello"; logMessage = () => { console.log(this.message); }; } -
在构造函数中绑定
this:this.logMessage = this.logMessage.bind(this);
泛型类
泛型允许我们创建可重用的类,这些类可以处理多种数据类型,同时保持类型安全。
class Box<T> {
private content: T;
constructor(initialContent: T) {
this.content = initialContent;
}
getContent(): T {
return this.content;
}
}
const stringBox = new Box<string>("Hello, Generics!");
const numberBox = new Box(123); // 类型可被推断
console.log(stringBox.getContent().toUpperCase()); // OK, toUpperCase() 存在
console.log(numberBox.getContent().toFixed(2)); // OK, toFixed() 存在