红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

TypeScript 学习笔记 6:Class

2026年2月26日 3点热度 0人点赞 0条评论

类成员

字段

创建一个没有任何属性的类:

class Point{}
const point = new Point();

添加属性:

class Point2{
    x: number; // 报错
    y: number; // 报错
}

TS 会进行检测,要求属性必须初始化,否则会报错:

class Point2{  // 创建类
    x: number = 0;  // 创建属性
    y: number = 0;  // 创建属性
}

直接初始化时,可以省略类型声明,因为 TS 的编译器可以自动推断出类型:

class Point3 {  
    x = 0;  // 推断出的类型为 number
    y = 0;  // 推断出的类型为 number
}

除了直接初始化,也可以通过构造器初始化:

class Point4 {
    x: number;
    y: number;
    constructor(x: number, y: number) {  // 创建构造函数
        this.x = x;  // 创建属性
        this.y = y;  // 创建属性
    }
}

如果类属性没有指定类型,也没有初始化,会被认为具有any类型,并且编辑器会有警告提示:

class Point5{
    x; // 警告,成员“x”隐式地具有“any”类型。
    y; // 警告,成员“y”隐式地具有“any”类型。
}

特殊的,如果类属性是由外部程序(比如第三方库)初始化的,为了绕开 TS 编辑器的报错信息,可以:

class Point6{
    x!: number;
    y!: number;
}

属性名称后添加!即可。

readonly

如果属性被标记为readonly,则只能在声明时初始化或在构造器中赋值,不能再其他地方被修改:

class Person{
    readonly name: string = 'new person';
    constructor(name?: string){
        if(name){
            this.name = name;
        }
    }
    setName(name: string): void{
        this.name = name; // 报错,无法为“name”赋值,因为它是只读属性
    }
}
​
const p = new Person('Tom');
console.log(p.name);
p.name = 'Jerry'; // 报错,无法为“name”赋值,因为它是只读属性

构造函数

构造函数与普通函数类似,也可以进行重载:

class Point7 {
    readonly x: number;
    readonly y: number;
    constructor(x: number, y: number);
    constructor(x: string, y: string);
    constructor(x: number | string, y: number | string) {
        if (typeof x === 'string') {
            this.x = Number(x)
        }
        else {
            this.x = x
        }
        if (typeof y === 'string') {
            this.y = Number(y)
        }
        else {
            this.y = y
        }
    }
}
​
const p2 = new Point7(1, 2);
const p3 = new Point7('1', '2');
console.log(p2);
console.log(p3);

与普通函数不同的地方是:

  • 不能使用类型参数。

  • 不能为返回值添加类型注解。

super 调用

如果存在继承关系,子类的构造函数中必须先使用super()调用父类的构造函数:

class Animal{}
​
class Dog extends Animal{
    owner: string;
    constructor(owner: string){
        super();
        this.owner = owner;
    }
}
​
const dog = new Dog('Tom');

方法

类中的方法与普通函数类似,需要注意的是,方法中使用类的其他属性都需要使用this.进行访问,否则使用的会是外部空间的变量:

const name = 'Bruce';
​
class Person2 {
    name: string;
    age: number;
    constructor(name: string, age: number = 20) {
        this.name = name;
        this.age = age;
    }
    test(){
        console.log(name); // 这里的 name 是 Bruce
    }
}

Getter/Setter

TS 支持为类属性添加 Getter/Setter:

class Person3{
    _name: string = '';
    get name(): string{
        console.log('getter');
        return this._name;
    }
    set name(value: string){
        console.log('setter');
        this._name = value;
    }
}
​
const p7 = new Person3();
p7.name = 'Bruce';
console.log(p7.name);

添加了 Getter/Setter 的属性名称与 Getter/Setter 的名称不能相同,习惯上,添加了 Getter/Setter 的属性名称以_开头(这种命名风格有点像 Python 的受保护属性)。

Getter 的返回值类型注解和 Setter 的参数类型注解都不是必须的,缺省时前者会以实际返回类型为准,后者会以属性类型为准。

Getter/Setter 的用途在于对已有代码的扩展,比如为已有属性添加读取或赋值时的额外功能,所以单纯的读取和改写操作(比如示例中的)的 Getter/Setter 是无意义的,此时应该公开属性,而不是使用 Getter/Setter。

如果只有 Getter 没有 Setter,TS 会认为对应的属性是readonly。

从 TS 4.3 开始,Setter 可以接收与原始属性类型不同的参数:

class Person4 {
    _name: string = '';
    set name(value: string | number) {
        if (typeof value === 'number') {
            if (value > 0) {
                this._name = 'Tom';
            }
            else {
                this._name = 'Jerry';
            }
        }
        else {
            this._name = value;
        }
    }
    get name(): string {
        return this._name;
    }
}
​
let person4 = new Person4();
person4.name = 1;
console.log(person4.name); // Tom
person4.name = -1;
console.log(person4.name); // Jerry

索引签名

像对象类型那样,类同样可以添加索引签名:

class CheckList {
    [key: string]: boolean | ((key: string) => boolean);
    check(key: string): boolean {
        return this[key] as boolean;
    }
}

继承

implements

implements表明某个类符合某个接口定义:

interface Flyable {
    fly(): void;
}
​
class Bird implements Flyable {
    fly() {
        console.log('Bird is flying');
    }
}
​
const bird = new Bird();
bird.fly();

TS 中implements仅能说明类与目标接口是否类型匹配,和其他语言中的 OOP 概念略有不同,比如:

interface PointInterface{
    x: number;
    y?: number;
}
​
class Point8 implements PointInterface{
    x: number = 0;
}
​
const point2 = new Point8();
point2.x = 10;
point2.y = 20; // 报错,类型“Point8”上不存在属性“y”

这里PointInterface中y是可选属性,而Point8虽然实现了PointInterface接口,但并没有定义y属性(连可选属性都没有)。虽然看上去有点古怪,但这依然符合条件,因为PointInterface接口中的y是可选的。

最终的效果就是Point8中没有y属性,但依然实现了PointInterface接口,最终point2.y调用就会报错。

extends

TS 中可以从一个类派生出它的子类:

class Animal2 {
    age: number = 1;
    eat(food: string) { 
        console.log('吃:' + food);
    }
}
​
class Dog2 extends Animal2 {
    bark() { }
}

方法重写

TS 中子类同样可以重写父类的方法:

class Animal2 {
    age: number = 1;
    eat(food: string) { 
        console.log('吃:' + food);
    }
}
​
class Cat2 extends Animal2 {
    meow() { }
    eat(food: string, where?: string) { 
        if(where === undefined){
            super.eat(food);
        }
        else{
            console.log('吃:' + food + ',在:' + where);
        }
    }
}
​
const cat1 = new Cat2();
cat1.eat('猫粮');
cat1.eat('猫粮', '猫窝');

重写时方法签名并不需要完全一致,只要子类的方法签名“兼容”父类的方法签名即可,通常是使用可选参数并使用super.xxx调用父类的方法,就像上面示例中做的。

之所以这样做,是要允许下面这样的调用:

const cat2: Animal2 = new Cat2();
cat2.eat('猫粮');

仅类型字段声明

看一个示例:

class Animal3{
    readonly kind: string = '';
}
​
class Cat3 extends Animal3{
    readonly kind: string = '猫科';
}
​
class AnimalHouse3{
    hold: Animal3;
    constructor(animal: Animal3){
        this.hold = animal;
    }
}
​
class CatHouse3 extends AnimalHouse3{
    constructor(cat: Cat3){
        super(cat);
    }
}
​
const cat3 = new Cat3();
console.log(cat3);
​
const catHouse3 = new CatHouse3(cat3);
console.log(catHouse3);
console.log(catHouse3.hold); // 这里 catHourse3.hold 是 Animal3 类型

catHouse3.hold属性是从AnimalHouse3继承的,因此其类型是Animal3,我们可以重写这个属性:

class CatHouse3 extends AnimalHouse3{
    declare hold: Cat3;
    // ...
}

但要注意,这里需要添加declare关键字,表明这里仅是声明一个字段,否则这里会因为编译器没有找到初始化而报错。

初始化顺序

class Parent{
    age: number = 1;
    constructor(){
        console.log(this.age);
    }
}
​
class Child extends Parent{
    age: number = 2;
    constructor(){
        super();
        console.log(this.age);
    }
}
​
const child = new Child();

上面这个示例会先输出1再输出2。

这是因为 JS 中,对象的初始化顺序是:

  1. 基类字段初始化。

  2. 基类的构造函数被调用。

  3. 子类的字段初始化。

  4. 子类的构造函数被调用。

成员可见性

TS 提供和其他语言类似的成员可见性标识符:

class Parent2{
    private privateAttr: number = 1;
    protected protectedAttr: number = 2;
    public publicAttr: number = 3;
    defaultAttr: number = 4;
}
​
const parent2 = new Parent2();
console.log(parent2.defaultAttr);
console.log(parent2.publicAttr);
console.log(parent2.protectedAttr); // 报错
console.log(parent2.privateAttr); // 报错

与Java不同,TS 中如果没有指定可见性,默认的可见性是public,这也和 JS 的默认行为一致。

如果存在继承关系,且子类重写了父类方法,可以放大该方法的可见性:

class parent3{
    protected func1(): void{
        console.log('parent.func1');
    }
}
​
class Child3 extends parent3{
    public func1(): void{
        console.log('child.func1');
    }
}
​
const child3 = new Child3();
child3.func1();

最后要强调的是,TS 是一种预处理语言,它只在将 TS 编译为 JS 的阶段生效,并不会影响 JS 的执行阶段,因此这些成员可见性标识符只能在编码阶段提供访问控制,并不能实际控制运行阶段(JS)的数据访问安全。

静态成员

TS 中同样可以为类定义静态成员,包括静态属性和静态方法,这些静态成员可以通过类名访问:

class Parent4{
    static staticAttr: number = 1;
    static staticFunc(): void{
        console.log('staticFunc');
    }
}
​
console.log(Parent4.staticAttr);
Parent4.staticFunc();

静态成员同样可以使用访问控制标识符:

class Parent5{
    private static staticAttr: number = 1;
}
console.log(Parent5.staticAttr); // 报错

静态成员同样可以被继承和重写:

class Parent6{
    static staticAttr: number = 1;
    static staticFunc(): void{
        console.log('parent.staticFunc', this.staticAttr);
    }
}
​
class Child6 extends Parent6{
    static staticAttr: number = 2;
    static staticFunc(): void{
        console.log('child.staticFunc', this.staticAttr);
    }
}
​
Parent6.staticFunc(); // parent.staticFunc 1
Child6.staticFunc(); // child.staticFunc 2

特殊静态名称

某些特殊名称是不能被定义为静态成员的:

class MyClass{
    static name = 'MyClass';
    // 报错,静态属性“name”与构造函数“MyClass”的内置属性函数“name”冲突
    static length = 10;
    // 静态属性“length”与构造函数“MyClass”的内置属性函数“length”冲突
}

这是因为类原型(prototype)实际上是一个函数类型(Function),它本身拥有一些属性,这些属性是不能被覆盖的。

没有静态类

TS 和 JS 中不存在静态类(static class xxx{}),但在大部分情况下不会影响使用。在 TS 和 JS 中,函数本身就是顶层元素,你可以在最外层定义函数并在任何地方使用,而不需要用静态类包围和定义:

function myFunc() {
    console.log('myFunc');
}
​
class MyClass2 {
    func1() {
        myFunc();
    }
}
​
const myClass2 = new MyClass2();
myClass2.func1();

静态初始化块

可以在类中添加静态初始化块:

class MyClass3 {
    static a: number;
    static {
        MyClass3.a = 1;
    }
}
​
console.log(MyClass3.a);

泛型类

类同样可以使用类型参数:

class Box<T>{
    value: T;
    constructor(value: T) {
        this.value = value;
    }
}
​
const box1 = new Box('hello'); // 类型推断为 Box<string>

这种情况下类型参数会根据new时候的构造器参数类型进行推断。

静态成员中的类型参数

不能将类类型参数用于静态成员,这是一个错误:

class MyClass4<T> {
    static a: T; // 报错,静态成员不能引用类类型参数
}

这也很容易理解,因为类类型参数只能通过创建实例时候(new xxx(...))的构造器参数类型进行推断,这和静态成员(类实例)无关。

this

JS 的 this 在某些时候会有一些与预期不符的表现:

class MyClass5{
    name: string = 'MyClass5';
    getName(): string {
        return this.name;
    }
}
​
const myClass5 = new MyClass5();
​
class MyClass6{
    name: string = 'MyClass6';
    getName = myClass5.getName;
}
​
const myClass6 = new MyClass6();
console.log(myClass6.getName()); // MyClass6

MyClass5中有一个方法getName,返回类属性name,MyClass6中同样有一个getName方法,不过没有直接定义,而是通过MyClass5的实例被赋值。

在最终调用MyClass6的实例的getName方法时,你可能期望输出的是MyClass5,而结果是MyClass6。因为在这种情况下,因为调用的实例是MyClass6的实例,因此此时的this关联的也是MyClass6的实例。

箭头函数

看一个例子:

class MyClass7{
    name: string = 'MyClass7';
    getName(): string {
        return this.name;
    }
}
​
const myClass7 = new MyClass7();
const getName = myClass7.getName;
console.log(getName()); 
// 报错,TypeError: Cannot read properties of undefined (reading 'name')

这里报错的原因和上面说的一样,因为getName函数是通过赋值直接关联到MyClass7实例的getName方法,因此调用时(getName()),this指向的是getName这个函数对象,而这个函数对象没有定义name属性。

注意,这里是一个隐蔽的运行时报错,编译阶段 TS 没有任何错误提示。

如果要解决这个问题,可以使用箭头函数定义类方法:

class MyClass8 {
    name: string = 'MyClass8';
    getName = () => {
        return this.name;
    }
}
​
const myClass8 = new MyClass8();
const getName2 = myClass8.getName;
console.log(getName2());

在箭头函数中,this只会指向所属的类实例,而不会因为赋值操作变成其它对象。

但是箭头函数存在其它问题:

class Parent7 {
    name: string = 'Parent7';
    getName = () => {
        return this.name;
    }
}
​
class Child7 extends Parent7 {
    name: string = 'Child7';
    getName = () => {
        super.getName(); 
        // 报错,父类字段“getName”无法通过 super 在子类中访问
        return this.name;
    }
}

就像示例展示的,如果父类的方法使用箭头函数定义,就不能在子类中使用super.xxx的方式获取该方法。

this 参数

另一种权衡后的解决方法是在需要使用this的方法中,添加一个特殊的this参数:

class Parent8 {
    name: string = 'parent8';
    getName(this: Parent8) {
        return this.name;
    }
}
​
const parent8 = new Parent8();
const getName3 = parent8.getName;
console.log(getName3());
// 报错,类型为“void”的 “this” 上下文不能分配给类型为“Parent8”的方法的 “this”。

这个参数比较特殊,TS 可以用它检测实际指向是否与预期类型相符,如果不符合就会报错。与之前的报错相比,此时报错会发生在编译阶段(IDE 会提示),而不是运行阶段,这相当有意义。

当然,这个特殊的this参数对于 JS 是没有意义的,理所当然的,编译后的 JS 代码中这个参数是不存在的。

特殊类型 this

TS 中,有一个特殊类型this,它表示当前类的类型:

class Parent9 {
    name: string = 'parent9';
    setName(newName: string){ // 这里推断出的返回值类型是 this
        this.name = newName;
        return this;
    }
}

与固定的类型Parent9相比,this类型是动态的,比如:

class Child9 extends Parent9 {
}
​
const child9 = new Child9();
const child9Name = child9.setName('child9'); // 推断类型为 Child9

可以看到,在扩展的子类中,this会变成具体的子类类型。

除了将this应用于返回值,同样可以应用于参数,看一个示例:

class MyNumber{
    value: number;
    constructor(value: number) {
        this.value = value;
    }
    compare(other: MyNumber): -1 | 0 | 1 {
        return this.value === other.value ? 0 : this.value > other.value ? 1 : -1;
    }
}
​
class SubMyNumber extends MyNumber {
    name: string = 'SubMyNumber2';
    constructor(value: number) {
        super(value);
    }
}
​
const myNumber = new MyNumber(1);
const subMyNumber = new SubMyNumber(2);
console.log(myNumber.compare(subMyNumber));
console.log(subMyNumber.compare(myNumber));

这个例子中,compare的参数other使用了基类类型,这很自然,其子类自然也符合基类类型,因此可以互相比较。

但如果使用this,就会变得很微妙:

class MyNumber2{
    value: number;
    constructor(value: number) {
        this.value = value;
    }
    compare(other: this): -1 | 0 | 1 {
        return this.value === other.value ? 0 : this.value > other.value ? 1 : -1;
    }
}
​
class SubMyNumber2 extends MyNumber2 {
    name: string = 'SubMyNumber2';
    constructor(value: number) {
        super(value);
    }
}
​
const myNumber2 = new MyNumber2(1);
const subMyNumber2 = new SubMyNumber2(2);
console.log(myNumber2.compare(subMyNumber2));
console.log(subMyNumber2.compare(myNumber2));
// 报错,类型“MyNumber2”的参数不能赋给类型“SubMyNumber2”的参数

此时子类的compare方法只能接收子类(SubMyNumber2)类型的参数,而不能接收基类类型。

要注意,TS 的类型比较是基于鸭子类型,因此如果子类和基类属性完全一致,比如子类没有一个多余的name属性,上面的代码就不会报错。

基于 this 的类型守卫

可以使用 this 定义一些方法作为对象的类型守卫,以缩小对象的类型:

class Animal4 {
    kind: string = '';
    isDog(): this is Dog4 {
        return this.kind === 'dog';
    }
    isCat(): this is Cat4 {
        return this instanceof Cat4;
    }
}
​
class Dog4 extends Animal4 {
    kind: 'dog' = 'dog';
    bark(): void {
        console.log('bark');
    }
}
​
class Cat4 extends Animal4 {
    kind: 'cat' = 'cat';
    meow(): void {
        console.log('meow');
    }
}
​
const animal4: Animal4 = new Cat4();
if (animal4.isDog()) {
    animal4.bark(); // 类型推断为 Dog4
} else if (animal4.isCat()) {
    animal4.meow(); // 类型推断为 Cat4
}

另一个this类型守卫的应用是避免对可选成员的显式判断:

class Box10<T> {
    value?: T;
    hasValue(): this is { value: T } {
        return this.value !== undefined;
    }
}
​
const box10 = new Box10<string>();
box10.value = 'hello';
if (box10.hasValue()) {
    console.log(box10.value.toUpperCase());
}

参数属性

TS 提供一种特殊语法,成员属性可以不在类中声明,直接在构造函数中作为参数进行声明,这些参数会被正确设置为属性,并且可以使用访问修饰符:

class Person8{
    constructor(public name: string, public age: number) {
    }
}
​
const p8 = new Person8('Alice', 18);
console.log(p8.name, p8.age);

类表达式

类表达式与类声明基本一致,唯一的区别是没有类名,我们需要使用其绑定的变量名创建实例:

const Person9 = class {
    constructor(public name: string, public age: number) {
    }
}
const p9 = new Person9('Alice', 18);
console.log(p9.name, p9.age);

构造函数签名

工具类型InstanceType可以获取类实例的类型:

class Person10 {
    constructor(public name: string, public age: number) {
    }
}
​
type PersonInstanceType = InstanceType<typeof Person10>;
function printPerson(person: PersonInstanceType){
    console.log(person.name, person.age);
}
​
const p10 = new Person10('Alice', 20);
printPerson(p10);

抽象类

TS 中可以定义抽象类和抽象方法:

abstract class Conveyance{
    abstract start(): void;
    abstract stop(): void;
    move(): void{
        this.start();
        this.stop();
    }
}
​
class Car extends Conveyance{
    start(): void{
        console.log('Car started');
    }
    stop(): void{
        console.log('Car stopped');
    }
}
​
class Bike extends Conveyance{
    start(): void{
        console.log('Bike started');
    }
    stop(): void{
        console.log('Bike stopped');
    }
}
​
const car = new Car();
car.move();
const bike = new Bike();
bike.move();

抽象构造签名

假设需要定义一个工厂函数创建抽象类的子类,具体创建哪个子类通过参数传递类类型:

function careteConveyance(ConceyanceType: typeof Conveyance): Conveyance{
    return new ConceyanceType(); // 报错,无法创建抽象类的实例
}
​
const car2 = careteConveyance(Car);
const bike2 = careteConveyance(Bike);

这里会报错,因为Conveyance是一个抽象类,不能使用new ConceyanceType()。

可以换一种方式:

function careteConveyance(ConceyanceType: new () => Conveyance): Conveyance {
    return new ConceyanceType();
}
​
const car2 = careteConveyance(Car);
const bike2 = careteConveyance(Bike);
car2.move();
bike2.move();

类之间的关系

再次强调,TS 判断某个类是否是另一个类(IS 关系),是通过鸭子类型判断的。因此即使两个类没有显式继承关系,也可能隐式具备 IS 关系:

class Person11 {
    name: string = '';
    age: number = 0;
}
​
class Person12 {
    name: string = '';
    age: number = 0;
}
​
const person12: Person12 = new Person11();

最特别的是,空对象{}不具备任何成员,因此它可以作为任何对象的基类:

class Empty { }
​
function dealEmpty(empty: Empty): void {
    console.log(empty);
}
​
dealEmpty(new Empty());
dealEmpty(new Person11());
dealEmpty({ age: 18 });

当然,不建议这么做。

The End.

参考资料

  • TypeScript 官方手册

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: ts
最后更新:2026年2月26日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号