类的成员

类的成员即类的各个组成部分,在 TypeScript 中,类的成员包括属性、方法、构造函数、存取器和索引成员等。接下来,将分别介绍。

属性

在类中可以定义属性,并在实例化对象后读写属性的值,示例代码如下。

class Guest {
    name: string;
}

let guest1: Guest = new Guest();
guest1.name = "Shark";

当为实例化后的对象的属性赋值时,该值必须要符合属性的类型,否则会引起编译错误,示例代码如下。

//编译错误:不能将类型"number"分配给类型"string"。ts(2322)
guest1.name = 1;
  1. 属性的初始值

    在类中,通过 “属性名称:属性类型=属性值” 为属性指定初始值。例如,在以下代码中,为 Guest 类的 name 属性指定了初始值,之后在实例化类的对象时,对象的 name 属性将具备该初始值。

    class Guest {
        name: string = "unknown";
    }
    
    let guest1: Guest = new Guest();
    console.log(guest1.name); //输出"unknown"

    除在属性定义部分指定初始值之外,还可以在构造函数中指定初始值,示例代码如下。

    class Guest {
        name: string;
    
        constructor() {
            this.name = "unknown";
        }
    }
    
    let guest1: Guest = new Guest();
    console.log(guest1.name); //输出"unknown"
  2. 只读属性

    通过 readonly 关键字将类的属性设置为只读属性,示例代码如下。

    class Guest {
        readonly authority: string = "GuestUser";
    }

    一旦为只读属性指定初始值,就无法再次赋值。例如,以下代码将引起编译错误。

    let guest1: Guest = new Guest();
    //编译错误:无法分配到 "authority",因为它是只读属性。ts(2540)
    guest1.authority = "AdminUser";

    通过构造函数为只读属性指定初始值,示例代码如下。

    class Guest {
        readonly authority: string;
    
    
        constructor() {
            this.authority = "GuestUser"
        }
    }

    但是,无法在非构造函数中为只读属性赋值,例如,以下代码将引起编译错误。

    class Guest {
        readonly authority: string;
    
        init(){
            //编译错误:无法分配到 "authority",因为它是只读属性。ts(2540)
            this.authority = "GuestUser"
        }
    }

方法

当以函数作为类或对象的成员时,这种函数通常称为方法。前面已经有一些定义并使用方法的示例,这里不再讲述,而介绍使用方法的一些要点。

在类或对象的方法中,如果要使用其他实例成员,必须加上 this 关键字。This 关键字表示调用当前类的实例对象,“this.成员名称” 则表示调用当前实例对象的成员。

例如,以下代码在各个方法中使用 name 属性及 reName() 方法时,都加上了 this 关键字。

class Person {
    name: string;

    constructor(initName: string) {
        this.reName(initName);
    }

    reName(targetName: string) {
        this.name = targetName;
    }

    hiddenIdentity() {
        this.reName("");
    }
}

如果不使用 this 关键字,将无法找到各个成员,从而引起编译错误,示例代码如下。

class Person {
    name: string;

    constructor(initName: string) {
        //编译错误:找不到名称"reName"。你的意思是实例成员"this.reName"?ts(2663)
        reName(initName);
    }

    reName(targetName: string) {
        //编译错误:找不到名称"name"。你的意思是实例成员"this.name"?ts(2663)
        name = targetName;
    }

    hiddenIdentity() {
        //编译错误:找不到名称"reName"。你的意思是实例成员"this.reName"?ts(2663)
        reName("");
    }
}

方法中的参数名称可以和成员属性的名称相同,但它们实际上是不同的变量,它们的使用方式有所区别。例如,在以下代码中,Person 类中定义了一个 name 属性,而 reName() 方法中定义了一个名为 name 的参数,在它的方法体中将 name 参数的值赋给了 name 属性(注意,当使用属性时必须加上 this 关键字)。

class Person {
    name: string;

    reName(name: string) {
        this.name = name;
    }
}

let person1: Person = new Person();
person1.reName("Rick");

方法可以声明为重载方法。例如,以下代码中的 CombineCaculator 类拥有一个 combine() 方法,该方法拥有 3 个重载签名,其两个参数和一个返回值分别为布尔类型、字符串类型、数值类型的值。而方法的重载实现部分的参数类型和返回值类型都定义成 any,以兼容前面所有签名的参数和返回值类型。然后,通过类型运算符判断传入的参数值的类型,并针对不同类型进行处理。

class CombineCaculator {
    combine(a: boolean, b: boolean): boolean;
    combine(a: string, b: string): string;
    combine(a: number, b: number): number;
    combine(a: any, b: any): any {
        if (typeof a == "boolean" && typeof b == "boolean") {
            return a || b;
        }
        else {
            return a + b;
        }
    }
}

上述方法拥有 3 个参数和返回值类型均不同的重载方法,因此在实例化 CombineCaculator 类的对象后,用对应的 3 种方式调用,示例代码如下。

let combineCaculator1 = new CombineCaculator();
//value1为number类型,值为3
let value1 = combineCaculator1.combine(1, 2);
//value2为string类型,值为"ab"
let value2 = combineCaculator1.combine("a", "b");
//value3为boolean类型,值为true
let value3 = combineCaculator1.combine(true, false);

构造函数

构造函数与普通方法非常相似,但它们存在一些区别。首先,构造函数无法像普通方法那样可以随时调用,它主要用于创建新对象,只有在实例化对象时才会调用。其次,构造函数不能定义返回值的类型,因为构造函数的返回值是以该类为模板的新实例对象。

构造函数的参数列表与普通方法的相同,它也可以指定默认参数、可选参数和剩余参数,示例代码如下。

class Task {
    taskName: string;
    prority: number;
    infomations: string[]

    constructor(taskName: string = "default task", prority?: number, ...infomations:
    string[]) {
        this.taskName = taskName;
        this.prority = prority;
        this.infomations = infomations;
    }
}

let task1: Task = new Task("Fuction1 Coding", 1, "需要单元测试", "需要重构", "用指定算法实现");

构造函数也支持重载。例如,在以下代码中,Task 类的构造函数拥有 3 个重载签名,其参数数量和类型各有区别,而构造函数的重载实现部分的参数数量及类型均能兼容前面所有的重载签名,然后通过类型运算符判断传入的参数值的类型,并针对不同类型进行处理。

class Task {
    taskName: string;
    prority: number;
    dueDate: Date;

    constructor(taskName: string);
    constructor(taskName: string, prority: number);
    constructor(taskName: string, dueDate: Date);
    constructor(taskName: string, prorityOrdueDate?: number | Date) {
        this.taskName = taskName;
        if (prorityOrdueDate) {
            if (typeof prorityOrdueDate == "number")
                this.prority = prorityOrdueDate;
            else
                this.dueDate = prorityOrdueDate;
        }
    }
}

Task 类拥有 3 个参数类型不同的构造函数,因此用对应的 3 种方式来实例化 Task 类的对象,示例代码如下。

let task1 = new Task("coding task");
let task2 = new Task("design task", 1);
let task3 = new Task("testing task", new Date(2022, 12, 31));

存取器

在类中可以定义属性,但有时并不会直接读写这些属性,而会通过一些自定义逻辑产生最终写入或读取的值。此时就将对属性的逻辑处理封装到存取器中,而从外部读写时,直接使用存取器名称即可。

存取器分为读方法(使用 get 关键字)与写方法(使用 set 关键字)两部分。读方法不接受任何参数,写方法只接受一个参数。存取器的声明语法如下。

class 类名 {
    ...
    get 存取器名称1()
    set 存取器名称1(参数名称:参数类型)
    get 存取器名称2()
    set 存取器名称2(参数名称:参数类型)
    ...
}

关于存取器的示例代码如下。

class Person {
    _name: string;
    get name() {
        return this._name;
    }
    set name(value: string) {
        this._name = value;
    }
}

Person 类中定义一个存取器 name,它同时支持读和写,读取时会返回 _name 属性的值,写入时会将 _name 属性设置为传入的值。

存取器的操作方式和普通属性没有差别,你可以用属性的操作方式进行读写,相比调用方法来说更简便。例如,以下代码通过 name 存取器为 person1 对象的 _name 属性写入了一个字符串 “Rick”,然后读取 name 存取器,输出该字符串。

let person1: Person = new Person();
person1.name = "Rick";
//以下代码输出"Rick"
console.log(person1.name);
//编译错误:不能将类型"number"分配给类型"string"。ts(2322)
person1.name = 1;

存取器通常不会用于以上代码所示的简单封装,而用于一些复杂逻辑的封装,通过这些逻辑产生最终写入或读取的值。例如,以下代码声明了一个 ExamResult 类,用于处理考试评级,_level 属性用于存放考试评级,isCheat 属性表示是否作弊。在 level 读存取器中,如果 isCheat==true,则直接返回 E 评级;否则,返回实际评级。在 level 写存取器中,不仅可以直接传入评级字符串,还可以传入一个分数,通过分数的范围产生评级字符串并将其值赋给 _level 属性。

class ExamResult {

    _level: string;
    isCheat: boolean = false;

    get level() {
        if (this.isCheat)
            return "E"
        else
            return this._level;
    }

    set level(value: string | number) {
        if (typeof value == "string") {
            this._level = value;
        }
        else {
            if (value > 90) this._level = "A";
            else if (value > 75) this._level = "B";
            else if (value > 60) this._level = "C"
            else this._level = "D";
        }
    }
}

let exam1: ExamResult = new ExamResult();
exam1.level = 99;
console.log(exam1.level); //输出"A"
exam1.isCheat = true;
console.log(exam1.level); //输出"E"

只读存取器/只写存取器

存取器可以定义为只读存取器或只写存取器。对于只读存取器,只定义 get() 方法,不定义 set() 方法;而对于只写存取器,只定义 set() 方法,不定义 get() 方法。

以下代码在 Person 类中定义了一个只读存取器 name,因此该存取器只能读取值而不能写入值,在实例化对象后,如果对该存取器进行写入操作,会引起编译错误。

class Person {
    _name: string;
    get name() {
        return this._name;
    }
}

let person1: Person = new Person();
//编译错误:无法分配到 "name",因为它是只读属性。ts(2540)
person1.name = "Rick";

以下代码在 Person 类中定义了一个只写存取器 name,在实例化对象时,不仅可以对其进行写入操作,还也可以进行读取操作,这不会引起编译错误,但读取出的值为 “undefined”。

class Person {
    _name: string;
    set name(value: string) {
        this._name = value;
    }
}

let person1: Person = new Person();
person1.name = "Rick";
console.log(person1.name); //输出undefined

索引成员

TypeScript 中,当类的对象的属性与方法比类中定义的属性与方法多时,如果对类中未定义的成员进行操作,就会引起编译错误,示例代码如下。类中只定义了 nameage 属性,但实例化对象后,对对象的 height 属性进行了操作,因此会引起编译错误。

class Person {
    name: string;
    age: number;
}

let person1: Person = new Person();
person1.name = "Kiddy";
person1.age = 17;
//编译错误:类型"Person"上不存在属性"height"。ts(2339)
person1.height = 180;

要解决这个问题,你可以用前面提到的可选修饰符(?)将 height 属性作为可选参数加入类的成员定义中,但这只适用于已知有哪些可选属性或方法的情况。在完全不确定有哪些可选属性或方法时该怎么办呢?这就需要用到索引签名,通过索引签名,让类支持任意数量的可选属性。索引签名的定义方式如下。

class 类名 {
    ...
    [索引名称:索引类型]:属性类型;
    ...
}

TypeScript 中只支持两种类型的索引——字符串索引和数值索引。以下代码是关于字符串索引的使用示例。定义了字符串索引后,你就可以使用任意成员的名称,示例中的索引支持存储 any 类型,因此可以支持任意类型的可选属性和方法。

class Person {
    name: string;
    age: number;
    [index: string]: any
}

let person1: Person = new Person();
person1.name = "Kiddy";
person1.age = 17;
//以下代码可以正常编译
person1.height = 180;
person1.company = "Newbility Inc."
person1.introduction = function () {
    console.log(`My name is ${this.name}`);
}