TypeScript 類(lèi)

2022-04-21 09:20 更新

傳統(tǒng)的JavaScript的程序使用函數(shù)和基于原型的繼承來(lái)創(chuàng)建可重用的組件,但對(duì)于熟悉使用面向?qū)ο蠓绞降某绦騿T來(lái)講就有些棘手,因?yàn)樗麄冇玫氖腔陬?lèi)的繼承并且對(duì)象是由類(lèi)構(gòu)建出來(lái)從ECMAScript 2015,也就是ECMAScript 6開(kāi)始,JavaScript程序員將能夠使用基于類(lèi)的面向?qū)ο蟮姆绞?。使用TypeScript,我們?cè)试S開(kāi)發(fā)者現(xiàn)在就使用這些特性,并且編譯后的JavaScript可以在所有主流瀏覽器和平臺(tái)上運(yùn)行,而不需要等到下個(gè)JavaScript的版本。

類(lèi)

下面看一個(gè)使用類(lèi)的例子:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

如果你使用過(guò)C?;騄ava,你會(huì)對(duì)這種語(yǔ)法非常熟悉。我們聲明一個(gè)  Greeter類(lèi)。這個(gè)類(lèi)有3個(gè)成員:一個(gè)叫做greeting的屬性,一個(gè)構(gòu)造函數(shù)和一個(gè)greet方法。

你會(huì)注意到,我們?cè)谝萌魏我粋€(gè)類(lèi)成員的時(shí)候都用了this。它表示我們?cè)L問(wèn)的是類(lèi)的成員。

最后一行,我們使用new構(gòu)造了Greeter類(lèi)的一個(gè)實(shí)例。它會(huì)調(diào)用之前定義的構(gòu)造函數(shù),創(chuàng)建一個(gè)  Greeter類(lèi)型的新對(duì)象,并執(zhí)行構(gòu)造函數(shù)初始化它。

繼承

在TypeScript里,我們可以使用常用的面向?qū)ο竽J健.?dāng)然,基于類(lèi)的程序設(shè)計(jì)中最基本的模式是允許使用繼承來(lái)擴(kuò)展現(xiàn)有的類(lèi)。

看下面的例子:

class Animal {
    name:string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

這個(gè)例子展示了TypeScript中繼承的一些特征,它們與其它語(yǔ)言類(lèi)似。 我們使用 extends關(guān)鍵字來(lái)創(chuàng)建子類(lèi)。你可以看到HorseSnake類(lèi)是基類(lèi)Animal的子類(lèi),并且可以訪(fǎng)問(wèn)其屬性和方法。

包含構(gòu)造函數(shù)的派生類(lèi)必須調(diào)用super(),它會(huì)執(zhí)行基類(lèi)的構(gòu)造方法。

這個(gè)例子演示了如何在子類(lèi)里可以重寫(xiě)父類(lèi)的方法。 Snake類(lèi)和Horse類(lèi)都創(chuàng)建了move方法,它們重寫(xiě)了從Animal繼承來(lái)的move方法,使得move方法根據(jù)不同的類(lèi)而具有不同的功能。 注意,即使 tom被聲明為Animal類(lèi)型,但因?yàn)樗闹凳?code>Horse,tom.move(34)會(huì)調(diào)用Horse里的重寫(xiě)方法:

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

公共,私有與受保護(hù)的修飾符

默認(rèn)為public

在上面的例子里,我們可以自由的訪(fǎng)問(wèn)程序里定義的成員。 如果你對(duì)其它語(yǔ)言中的類(lèi)比較了解,就會(huì)注意到我們?cè)谥暗拇a里并沒(méi)有使用 public來(lái)做修飾;例如,C#要求必須明確地使用public指定成員是可見(jiàn)的。 在TypeScript里,成員都默認(rèn)為 public

你也可以明確的將一個(gè)成員標(biāo)記成public。 我們可以用下面的方式來(lái)重寫(xiě)上面的 Animal類(lèi):

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

理解private

當(dāng)成員被標(biāo)記成private時(shí),它就不能在聲明它的類(lèi)的外部訪(fǎng)問(wèn)。比如:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // Error: 'name' is private;

TypeScript使用的是結(jié)構(gòu)性類(lèi)型系統(tǒng)。 當(dāng)我們比較兩種不同的類(lèi)型時(shí),并不在乎它們從何處而來(lái),如果所有成員的類(lèi)型都是兼容的,我們就認(rèn)為它們的類(lèi)型是兼容的。

然而,當(dāng)我們比較帶有privateprotected成員的類(lèi)型的時(shí)候,情況就不同了。 如果其中一個(gè)類(lèi)型里包含一個(gè) private成員,那么只有當(dāng)另外一個(gè)類(lèi)型中也存在這樣一個(gè)private成員, 并且它們都是來(lái)自同一處聲明時(shí),我們才認(rèn)為這兩個(gè)類(lèi)型是兼容的。 對(duì)于 protected成員也使用這個(gè)規(guī)則。

下面來(lái)看一個(gè)例子,更好地說(shuō)明了這一點(diǎn):

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // Error: Animal and Employee are not compatible

這個(gè)例子中有AnimalRhino兩個(gè)類(lèi),RhinoAnimal類(lèi)的子類(lèi)。 還有一個(gè) Employee類(lèi),其類(lèi)型看上去與Animal是相同的。 我們創(chuàng)建了幾個(gè)這些類(lèi)的實(shí)例,并相互賦值來(lái)看看會(huì)發(fā)生什么。 因?yàn)?nbsp;AnimalRhino共享了來(lái)自Animal里的私有成員定義private name: string,因此它們是兼容的。 然而 Employee卻不是這樣。當(dāng)把Employee賦值給Animal的時(shí)候,得到一個(gè)錯(cuò)誤,說(shuō)它們的類(lèi)型不兼容。 盡管 Employee里也有一個(gè)私有成員name,但它明顯不是Animal里面定義的那個(gè)。

理解protected

protected修飾符與private修飾符的行為很相似,但有一點(diǎn)不同,protected成員在派生類(lèi)中仍然可以訪(fǎng)問(wèn)。例如:

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error

注意,我們不能在Person類(lèi)外使用name,但是我們?nèi)匀豢梢酝ㄟ^(guò)Employee類(lèi)的實(shí)例方法訪(fǎng)問(wèn),因?yàn)?code>Employee是由Person派生而來(lái)的。

構(gòu)造函數(shù)也可以被標(biāo)記成protected。 這意味著這個(gè)類(lèi)不能在包含它的類(lèi)外被實(shí)例化,但是能被繼承。比如,

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee can extend Person
class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // Error: The 'Person' constructor is protected

readonly修飾符

你可以使用readonly關(guān)鍵字將屬性設(shè)置為只讀的。 只讀屬性必須在聲明時(shí)或構(gòu)造函數(shù)里被初始化。

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.

參數(shù)屬性

在上面的例子中,我們不得不定義一個(gè)受保護(hù)的成員name和一個(gè)構(gòu)造函數(shù)參數(shù)theNamePerson類(lèi)里,并且立刻給nametheName賦值。 這種情況經(jīng)常會(huì)遇到。 參數(shù)屬性可以方便地讓我們?cè)谝粋€(gè)地方定義并初始化一個(gè)成員。 下面的例子是對(duì)之前 Animal類(lèi)的修改版,使用了參數(shù)屬性:

class Animal {
    constructor(private name: string) { }
    move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

注意看我們是如何舍棄了theName,僅在構(gòu)造函數(shù)里使用private name: string參數(shù)來(lái)創(chuàng)建和初始化name成員。 我們把聲明和賦值合并至一處。

參數(shù)屬性通過(guò)給構(gòu)造函數(shù)參數(shù)添加一個(gè)訪(fǎng)問(wèn)限定符來(lái)聲明。 使用 private限定一個(gè)參數(shù)屬性會(huì)聲明并初始化一個(gè)私有成員;對(duì)于publicprotected來(lái)說(shuō)也是一樣。

存取器

TypeScript支持通過(guò)getters/setters來(lái)截取對(duì)對(duì)象成員的訪(fǎng)問(wèn)。 它能幫助你有效的控制對(duì)對(duì)象成員的訪(fǎng)問(wèn)。

下面來(lái)看如何把一個(gè)簡(jiǎn)單的類(lèi)改寫(xiě)成使用getset。 首先,我們從一個(gè)沒(méi)有使用存取器的例子開(kāi)始。

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

我們可以隨意的設(shè)置fullName,這是非常方便的,但是這也可能會(huì)帶來(lái)麻煩。

下面這個(gè)版本里,我們先檢查用戶(hù)密碼是否正確,然后再允許其修改員工信息。 我們把對(duì) fullName的直接訪(fǎng)問(wèn)改成了可以檢查密碼的set方法。 我們也加了一個(gè) get方法,讓上面的例子仍然可以工作。

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    alert(employee.fullName);
}

我們可以修改一下密碼,來(lái)驗(yàn)證一下存取器是否是工作的。當(dāng)密碼不對(duì)時(shí),會(huì)提示我們沒(méi)有權(quán)限去修改員工。

對(duì)于存取器有下面幾點(diǎn)需要注意的:

首先,存取器要求你將編譯器設(shè)置為輸出ECMAScript 5或更高。 不支持降級(jí)到ECMAScript 3。 其次,只帶有 get不帶有set的存取器自動(dòng)被推斷為readonly。 這在從代碼生成 .d.ts文件時(shí)是有幫助的,因?yàn)槔眠@個(gè)屬性的用戶(hù)會(huì)看到不允許夠改變它的值。

靜態(tài)屬性

到目前為止,我們只討論了類(lèi)的實(shí)例成員,那些僅當(dāng)類(lèi)被實(shí)例化的時(shí)候才會(huì)被初始化的屬性。 我們也可以創(chuàng)建類(lèi)的靜態(tài)成員,這些屬性存在于類(lèi)本身上面而不是類(lèi)的實(shí)例上。 在這個(gè)例子里,我們使用 static定義origin,因?yàn)樗撬芯W(wǎng)格都會(huì)用到的屬性。 每個(gè)實(shí)例想要訪(fǎng)問(wèn)這個(gè)屬性的時(shí)候,都要在 origin前面加上類(lèi)名。 如同在實(shí)例屬性上使用 this.前綴來(lái)訪(fǎng)問(wèn)屬性一樣,這里我們使用Grid.來(lái)訪(fǎng)問(wèn)靜態(tài)屬性。

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

抽象類(lèi)

抽象類(lèi)做為其它派生類(lèi)的基類(lèi)使用。 它們一般不會(huì)直接被實(shí)例化。 不同于接口,抽象類(lèi)可以包含成員的實(shí)現(xiàn)細(xì)節(jié)。abstract關(guān)鍵字是用于定義抽象類(lèi)和在抽象類(lèi)內(nèi)部定義抽象方法。

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

抽象類(lèi)中的抽象方法不包含具體實(shí)現(xiàn)并且必須在派生類(lèi)中實(shí)現(xiàn)。 抽象方法的語(yǔ)法與接口方法相似。 兩者都是定義方法簽名但不包含方法體。 然而,抽象方法必須包含 abstract關(guān)鍵字并且可以包含訪(fǎng)問(wèn)修飾符。

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // 必須在派生類(lèi)中實(shí)現(xiàn)
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // constructors in derived classes must call super()
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on declared abstract type

高級(jí)技巧

構(gòu)造函數(shù)

當(dāng)你在TypeScript里聲明了一個(gè)類(lèi)的時(shí)候,實(shí)際上同時(shí)聲明了很多東西。 首先就是類(lèi)的 實(shí)例的類(lèi)型。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

這里,我們寫(xiě)了let greeter: Greeter,意思是Greeter類(lèi)的實(shí)例的類(lèi)型是Greeter。 這對(duì)于用過(guò)其它面向?qū)ο笳Z(yǔ)言的程序員來(lái)講已經(jīng)是老習(xí)慣了。

我們也創(chuàng)建了一個(gè)叫做構(gòu)造函數(shù)的值。這個(gè)函數(shù)會(huì)在我們使用  new創(chuàng)建類(lèi)實(shí)例的時(shí)候被調(diào)用。下面我們來(lái)看看,上面的代碼被編譯成JavaScript后是什么樣子的:

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

上面的代碼里,let Greeter將被賦值為構(gòu)造函數(shù)。我們當(dāng)調(diào)用  new并執(zhí)行了這個(gè)函數(shù)后,便會(huì)得到一個(gè)類(lèi)的實(shí)例。這個(gè)構(gòu)造函數(shù)也包含了類(lèi)的所有靜態(tài)屬性。換個(gè)角度說(shuō),我們可以認(rèn)為類(lèi)具有  實(shí)例部分靜態(tài)部分這兩個(gè)部分。

讓我們稍微改寫(xiě)一下這個(gè)例子,看看它們之前的區(qū)別:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

這個(gè)例子里,greeter1與之前看到的一樣。我們實(shí)例化  Greeter類(lèi),并使用這個(gè)對(duì)象。與我們之前看到的一樣。

再之后,我們直接使用類(lèi)。我們創(chuàng)建33了一個(gè)叫做  greeterMaker的變量。這個(gè)變量保存了這個(gè)類(lèi)或者說(shuō)保存了類(lèi)構(gòu)造函數(shù)。然后我們使用  typeof Greeter,意思是取Greeter類(lèi)的類(lèi)型,而不是實(shí)例的類(lèi)型?;蛘吒_切的說(shuō), “我告訴  Greeter標(biāo)識(shí)符的類(lèi)型”,也就是構(gòu)造函數(shù)的類(lèi)型。這個(gè)類(lèi)型包含了類(lèi)的所有靜態(tài)成員和構(gòu)造函數(shù)。之后,就和前面一樣,在我們  greeterMaker上使用new,創(chuàng)建33 Greeter的實(shí)例。

把類(lèi)當(dāng)做接口使用

如上一節(jié)里所講的,類(lèi)定義會(huì)創(chuàng)建兩個(gè)東西:類(lèi)的實(shí)例類(lèi)型和一個(gè)構(gòu)造函數(shù)。因?yàn)轭?lèi)可以創(chuàng)建出類(lèi)型,所以你能夠在允許使用接口的地方使用類(lèi)。

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)