解读typescript中class类
TypeScript 中的类(Class)是面向对象编程(OOP)的核心,它基于 ES6 类的语法,并引入了类型系统和额外特性(如访问修饰符、抽象类等)。以下我为总结出了几点:
一、类的简单介绍
1. 定义类
class Person {
// 属性(需初始化或通过构造函数赋值)
name: string;
age: number;
// 构造函数
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// 方法
greet() {
console.log(`Hello, I'm ${this.name}.`);
}
}
// 使用
const alice = new Person("Alice", 25);
alice.greet(); // 输出:Hello, I'm Alice.
类的方法跟普通函数一样,可以使用参数默认值,以及函数重载(详情可阅读上一篇文章)。
class Point {
x: number;
y: number;
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
上面示例中,如果新建实例时,不提供属性x
和y
的值,它们都等于默认值0
。
下面是函数重载的例子。
class Point {
constructor(x:number, y:string);
constructor(s:string);
constructor(xs:number|string, y?:string) {
// ...
}
}
上面示例中,构造方法可以接受一个参数,也可以接受两个参数,采用函数重载进行类型声明。
2.存取器方法
存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。
它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。
class C {
_name = '';
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
}
上面示例中,get name()
是取值器,其中get
是关键词,name
是属性名。外部读取name
属性时,实例对象会自动调用这个方法,该方法的返回值就是name
属性的值。
set name()
是存值器,其中set
是关键词,name
是属性名。外部写入name
属性时,实例对象会自动调用这个方法,并将所赋的值作为函数参数传入。
TypeScript 对存取器有以下规则。
(1)如果某个属性只有get
方法,没有set
方法,那么该属性自动成为只读属性。
class C {
_name = 'foo';
get name() {
return this._name;
}
}
const c = new C();
c.name = 'bar'; // 报错
上面示例中,name
属性没有set
方法,对该属性赋值就会报错。
(2)set
方法的参数类型,必须兼容get
方法的返回值类型,否则报错。
class C {
_name = '';
get name():string {
return this._name;
}
set name(value:number) {
this._name = value; // 报错
}
}
上面示例中,get
方法的返回值类型是字符串,与set
方法参数类型不兼容,导致报错。
class C {
_name = '';
get name():string {
return this._name;
}
set name(value:number|string) {
this._name = String(value); // 正确
}
}
上面示例中,set
方法的参数类型(number|return
)兼容get
方法的返回值类型(string
),这是允许的。但是,最终赋值的时候,还是必须保证与get
方法的返回值类型一致。
另外,如果set
方法的参数没有指定类型,那么会推断为与get
方法返回值类型一致。
(3)get
方法与set
方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
3.属性索引
类允许定义属性索引。
class MyClass {
[s:string]: boolean |
((s:string) => boolean);
get(s:string) {
return this[s] as boolean;
}
}
上述代码表示:
- 表示该类的实例允许任意字符串类型的属性名
- 属性的值可以是两种类型:
boolean
类型- 函数类型:接收 string 参数并返回 boolean 的函数
这里给出几个示例方便大家理解:
const obj = new MyClass();
// 设置属性(符合索引签名要求)
obj.isActive = true; // ✅ 允许 boolean 值
obj.checkValid = (s: string) => s.length > 0; // ✅ 允许函数
// 使用 get 方法
console.log(obj.get('isActive')); // ✅ 输出 true
console.log(obj.get('checkValid')); // ❌ 运行时错误:函数无法被当作 boolean 使用
// 类型错误示例
obj.count = 10; // ❌ 类型错误:必须是 boolean 或函数类型
二、类与接口
1.implements 关键字
interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。
interface Country {
name:string;
capital:string;
}
// 或者
type Country = {
name:string;
capital:string;
}
class MyCountry implements Country {
name = '';
capital = '';
}
上面示例中,interface
或type
都可以定义一个对象类型。类MyCountry
使用implements
关键字,表示该类的实例对象满足这个外部类型。
类可以定义接口没有声明的方法和属性。
interface Point {
x: number;
y: number;
}
class MyPoint implements Point {
x = 1;
y = 1;
z:number = 1;
}
implements
关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。
class Car {
id:number = 1;
move():void {};
}
class MyCar implements Car {
id = 2; // 不可省略
move():void {}; // 不可省略
}
上面示例中,implements
后面是类Car
,这时 TypeScript 就把Car
视为一个接口,要求MyCar
实现Car
里面的每一个属性和方法,否则就会报错。所以,这时不能因为Car
类已经实现过一次,而在MyCar
类省略属性或方法。
2.多接口的实现
类的继承
class Car implements MotorVehicle {
}
class SecretCar extends Car implements Flyable, Swimmable {
}
上面示例中,Car
类实现了MotorVehicle
,而SecretCar
类继承了Car
类,然后再实现Flyable
和Swimmable
两个接口,相当于SecretCar
类同时实现了三个接口。
接口的继承
interface MotorVehicle {
// ...
}
interface Flyable {
// ...
}
interface Swimmable {
// ...
}
interface SuperCar extends MotoVehicle,Flyable, Swimmable {
// ...
}
class SecretCar implements SuperCar {
// ...
}
上面示例中,接口SuperCar
通过SuperCar
接口,就间接实现了多个接口。
3.类与接口的合并
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
class A {
x:number = 1;
}
interface A {
y:number;
}
let a = new A();
a.y = 10;
a.x // 1
a.y // 10
上面示例中,类A
与接口A
同名,后者会被合并进前者的类型定义。
三、Class 类型
1.实例类型
TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。
// 定义 Color 类
class Color {
name: string; // 声明类的字符串类型属性
// 构造函数:接收字符串参数,用于初始化 name 属性
constructor(name: string) {
this.name = name;
}
}
// 创建实例
const green: Color = new Color('green');
// 等效于:
// const green = new Color('green'); // TypeScript 可以自动推断类型
上述代码的实例化过程:
new Color('green') 的执行过程:
1. 创建空对象 {}
2. 调用构造函数 constructor('green')
3. 设置 this.name = 'green'
4. 返回初始化后的对象
类型检查示例:
green.name = 'red'; // ✅ 允许修改字符串值
green.name = 123; // ❌ 类型错误:不能将 number 赋值给 string
const wrong: Color = { name: 'blue' };
// ❌ 错误:虽然对象结构相同,但缺少构造函数初始化过程
2.结构类型原则
Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
class Foo {
id!:number;
}
function fn(arg:Foo) {
// ...
}
const bar = {
id: 10,
amount: 100,
};
fn(bar); // 正确
上面示例中,对象bar
满足类Foo
的实例结构,只是多了一个属性amount
。所以,它可以当作参数,传入函数fn()
。
如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。即使其中的一个类添加了一个属性,,TypeScript 也会认为是同一个类型。
class Person {
name: string;
}
class Customer {
name: string;
}
class Man {
name: string;
age: number;
}
// 正确
const cust:Customer = new Person();
// 正确
const cust:Customer = new Man();
但是注意,如果Man
类比Customer
类少一个属性age
,它就不满足Customer
类型的实例结构,就报错了。因为在使用Customer
类型的情况下,可能会用到它的age
属性,而Person
类就没有这个属性。
class Man{
name: string;
}
class Customer {
name: string;
age: number;
}
// 报错
const cust:Customer = new Man();
不仅是类,如果某个对象跟某个 class 的实例结构相同,TypeScript 也认为两者的类型相同。
注意,确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
class Point {
x: number;
y: number;
static t: number;
constructor(x:number) {}
}
class Position {
x: number;
y: number;
z: number;
constructor(x:string) {}
}
const point:Point = new Position('');
上面示例中,Point
与Position
的静态属性和构造方法都不一样,但因为Point
的实例成员与Position
相同,所以Position
兼容Point
。
四、类的继承
类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
class A {
greet() {
console.log('Hello, world!');
}
}
class B extends A {
}
const b = new B();
b.greet() // "Hello, world!"
子类可以覆盖基类的同名方法。
class B extends A {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name}`);
}
}
}
但是,子类的同名方法不能与基类的类型定义相冲突。
class A {
greet() {
console.log('Hello, world!');
}
}
class B extends A {
// 报错
greet(name:string) {
console.log(`Hello, ${name}`);
}
}
上面示例中,子类B
的greet()
有一个name
参数,跟基类A
的greet()
定义不兼容,因此就报错了。
五、访问修饰符
TypeScript 提供三种访问控制修饰符:
public
(默认):任何地方可访问。private
:仅类内部可访问。protected
:类内部和子类可访问。
public
public
修饰符表示这是公开成员,外部可以自由访问。
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
private
private
修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
class A {
private x:number = 0;
}
const a = new A();
a.x // 报错
class B extends A {
showX() {
console.log(this.x); // 报错
}
}
严格地说,
private
定义的私有成员,并不是真正意义的私有成员。一方面,编译成 JavaScript 后,private
关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问private
成员没有严格禁止,使用方括号写法([]
)或者in
运算符,实例对象就能访问该成员。
class A {
private x = 1;
}
const a = new A();
a['x'] // 1
if ('x' in a) { // 正确
// ...
}
由于private
存在这些问题,加上它是 ES6 标准发布前出台的,而 ES6 引入了自己的私有成员写法#propName
。因此建议不使用private
,改用 ES6 的写法,获得真正意义的私有成员。
class A {
#x = 1;
}
const a = new A();
a['x'] // 报错
protected
protected
修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
class A {
protected x = 1;
}
class B extends A {
getX() {
return this.x;
}
}
const a = new A();
const b = new B();
a.x // 报错
b.getX() // 1
子类不仅可以拿到父类的保护成员,还可以定义同名成员。
class A {
protected x = 1;
}
class B extends A {
x = 2;
}
上面示例中,子类B
定义了父类A
的同名成员x
,并且父类的x
是保护成员,子类将其改成了公开成员。B
类的x
属性前面没有修饰符,等同于修饰符是public
,外界可以读取这个属性。
在类的外部,实例对象不能读取保护成员,但是在类的内部可以。
class A {
protected x = 1;
f(obj:A) {
console.log(obj.x);
}
}
const a = new A();
a.x // 报错
a.f(a) // 1
六、静态成员
静态成员属于类本身而非实例,通过 static
定义:
class MathUtils {
static PI = 3.1415926;
static sum(a: number, b: number) {
return a + b;
}
}
console.log(MathUtils.PI); // 3.1415926
console.log(MathUtils.sum(1, 2)); // 3
静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
class Demo {
static staticVal = 10; // 静态属性
instanceVal = 20; // 实例属性
static x = 0;
static printX() {
console.log(Demo.x); // 通过类名访问静态属性
}
static staticMethod() {
console.log(this.staticVal); // ✅ 正确访问静态属性
console.log(this.instanceVal); // ❌ 错误:无法访问实例属性
}
}
// 错误访问方式
// 直接通过类名访问静态属性
Demo.x; // → 0
// 直接通过类名调用静态方法
Demo.printX(); // → 输出 0
// 正确访问方式
console.log(Demo.staticVal); // ✅ 10
const obj = new Demo();
console.log(obj.instanceVal); // ✅ 20
七、抽象类
用 abstract
定义抽象类和抽象方法,抽象类不可实例化,抽象方法必须由子类实现:
abstract class A {
id = 1;
}
const a = new A(); // 报错
抽象类只能当作基类使用,用来在它的基础上定义子类。
abstract class A {
id = 1;
}
class B extends A {
amount = 100;
}
const b = new B();
b.id // 1
b.amount // 100
例子实现;
abstract class Shape {
abstract area(): number;
printArea() {
console.log(`Area: ${this.area()}`);
}
}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle(5);
circle.printArea(); // 输出:Area: 78.5398...