TypeScript 類

2022-04-21 09:20 更新

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

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

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

let greeter = new Greeter("world");

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

你會注意到,我們在引用任何一個(gè)類成員的時(shí)候都用了this。它表示我們訪問的是類的成員。

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

繼承

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

看下面的例子:

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中繼承的一些特征,它們與其它語言類似。 我們使用 extends關(guān)鍵字來創(chuàng)建子類。你可以看到HorseSnake類是基類Animal的子類,并且可以訪問其屬性和方法。

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

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

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

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

默認(rèn)為public

在上面的例子里,我們可以自由的訪問程序里定義的成員。 如果你對其它語言中的類比較了解,就會注意到我們在之前的代碼里并沒有使用 public來做修飾;例如,C#要求必須明確地使用public指定成員是可見的。 在TypeScript里,成員都默認(rèn)為 public。

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

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í),它就不能在聲明它的類的外部訪問。比如:

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

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

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

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

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

理解protected

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

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類外使用name,但是我們?nèi)匀豢梢酝ㄟ^Employee類的實(shí)例方法訪問,因?yàn)?code>Employee是由Person派生而來的。

構(gòu)造函數(shù)也可以被標(biāo)記成protected。 這意味著這個(gè)類不能在包含它的類外被實(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類里,并且立刻給nametheName賦值。 這種情況經(jīng)常會遇到。 參數(shù)屬性可以方便地讓我們在一個(gè)地方定義并初始化一個(gè)成員。 下面的例子是對之前 Animal類的修改版,使用了參數(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ù)來創(chuàng)建和初始化name成員。 我們把聲明和賦值合并至一處。

參數(shù)屬性通過給構(gòu)造函數(shù)參數(shù)添加一個(gè)訪問限定符來聲明。 使用 private限定一個(gè)參數(shù)屬性會聲明并初始化一個(gè)私有成員;對于publicprotected來說也是一樣。

存取器

TypeScript支持通過getters/setters來截取對對象成員的訪問。 它能幫助你有效的控制對對象成員的訪問。

下面來看如何把一個(gè)簡單的類改寫成使用getset。 首先,我們從一個(gè)沒有使用存取器的例子開始。

class Employee {
    fullName: string;
}

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

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

下面這個(gè)版本里,我們先檢查用戶密碼是否正確,然后再允許其修改員工信息。 我們把對 fullName的直接訪問改成了可以檢查密碼的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);
}

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

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

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

靜態(tài)屬性

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

抽象類

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

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

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

abstract class Department {

    constructor(public name: string) {
    }

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

    abstract printMeeting(): void; // 必須在派生類中實(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

高級技巧

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

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

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());

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

我們也創(chuàng)建了一個(gè)叫做構(gòu)造函數(shù)的值。這個(gè)函數(shù)會在我們使用  new創(chuàng)建類實(shí)例的時(shí)候被調(diào)用。下面我們來看看,上面的代碼被編譯成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ù)后,便會得到一個(gè)類的實(shí)例。這個(gè)構(gòu)造函數(shù)也包含了類的所有靜態(tài)屬性。換個(gè)角度說,我們可以認(rèn)為類具有  實(shí)例部分靜態(tài)部分這兩個(gè)部分。

讓我們稍微改寫一下這個(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類,并使用這個(gè)對象。與我們之前看到的一樣。

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

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

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

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

interface Point3d extends Point {
    z: number;
}

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號