TypeScript 類

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

基本示例

下面看一個使用類的例子:

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

let greeter = new Greeter('world')

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

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

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

繼承

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

看下面的例子:

class Animal {
  move(distance: number = 0) {
    console.log(`Animal moved ${distance}m.`)
  }
}

class Dog extends Animal {
  bark() {
    console.log('Woof! Woof!')
  }
}

const dog = new Dog()
dog.bark()
dog.move(10)

這個例子展示了最基本的繼承:類從基類中繼承了屬性和方法。 這里,Dog 是一個 派生類,它派生自 Animal 基類,通過 extends 關(guān)鍵字。 派生類通常被稱作子類,基類通常被稱作超類。

因為 Dog 繼承了 Animal 的功能,因此我們可以創(chuàng)建一個 Dog 的實例,它能夠 bark()move()。

下面我們來看個更加復(fù)雜的例子。

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

class Snake extends Animal {
  constructor(name: string) { 
    super(name)
  }
  move(distance: number = 5) {
    console.log('Slithering...')
    super.move(distance)
  }
}

class Horse extends Animal {
  constructor(name: string) {
    super(name)
  }
  move(distance: number = 45) {
    console.log('Galloping...')
    super.move(distance)
  }
}

let sam = new Snake('Sammy')
let tom: Animal = new Horse('Tommy')

sam.move()
tom.move(34)

這個例子展示了一些上面沒有提到的特性。 這一次,我們使用 extends 關(guān)鍵字創(chuàng)建了 Animal的兩個子類:HorseSnake。

與前一個例子的不同點是,派生類包含了一個構(gòu)造函數(shù),它 必須調(diào)用 super(),它會執(zhí)行基類的構(gòu)造函數(shù)。 而且,在構(gòu)造函數(shù)里訪問 this 的屬性之前,我們 一定要調(diào)用 super()。 這個是 TypeScript 強(qiáng)制執(zhí)行的一條重要規(guī)則。

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

Slithering...
Sammy moved 5m.
Galloping...
Tommy moved 34m.

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

默認(rèn)為 public

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

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

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

理解 private

當(dāng)成員被標(biāo)記成 private 時,它就不能在聲明它的類的外部訪問。比如:

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

new Animal('Cat').name // 錯誤: 'name' 是私有的.

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

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

下面來看一個例子,更好地說明了這一點:

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

class Rhino extends Animal {
  constructor() { 
    super('Rhino')
  }
}

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

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

animal = rhino
animal = employee // 錯誤: Animal 與 Employee 不兼容.

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

理解 protected

protected 修飾符與 private 修飾符的行為很相似,但有一點不同,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
  }
  
  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 類的實例方法訪問,因為 Employee 是由 Person 派生而來的。

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

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

// Employee 能夠繼承 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') // 錯誤: 'Person' 的構(gòu)造函數(shù)是被保護(hù)的.

readonly 修飾符

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

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

let john = new Person('John')
john.name = 'peter'

參數(shù)屬性

在上面的例子中,我們必須在 Person 類里定義一個只讀成員 name 和一個參數(shù)為 name 的構(gòu)造函數(shù),并且立刻將 name 的值賦給 this.name,這種情況經(jīng)常會遇到。 參數(shù)屬性可以方便地讓我們在一個地方定義并初始化一個成員。 下面的例子是對之前 Person 類的修改版,使用了參數(shù)屬性:

class Person {
  constructor(readonly name: string) {
  }
}

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

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

存取器

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

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

class Employee {
  fullName: string
}

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

我們可以設(shè)置 fullName,因為它是 public 的,有時候當(dāng)我們?nèi)バ薷乃臅r候觸發(fā)一些額外邏輯,存取器就派上用場了。

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

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

對于存取器有下面幾點需要注意的:

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

靜態(tài)屬性

到目前為止,我們只討論了類的實例成員,那些僅當(dāng)類被實例化的時候才會被初始化的屬性。 我們也可以創(chuàng)建類的靜態(tài)成員,這些屬性存在于類本身上面而不是類的實例上。 在這個例子里,我們使用 static 定義 origin,因為它是所有網(wǎng)格都會用到的屬性。 每個實例想要訪問這個屬性的時候,都要在 origin 前面加上類名。 如同在實例屬性上使用 this.xxx 來訪問屬性一樣,這里我們使用 Grid.xxx 來訪問靜態(tài)屬性。

class Grid {
  static origin = {x: 0, y: 0}

  scale: number

  constructor (scale: number) {
    this.scale = scale
  }

  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
  }
}

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

console.log(grid1.calculateDistanceFromOrigin({x: 3, y: 4}))
console.log(grid2.calculateDistanceFromOrigin({x: 3, y: 4}))

抽象類

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

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

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

abstract class Department {
  name: string

  constructor(name: string) {
     this.name = name
  }

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

  abstract printMeeting(): void // 必須在派生類中實現(xiàn)
}

class AccountingDepartment extends Department {
  constructor() {
    super('Accounting and Auditing') // 在派生類的構(gòu)造函數(shù)中必須調(diào)用 super()
  }

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

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

let department: Department // 允許創(chuàng)建一個對抽象類型的引用
department = new Department() // 錯誤: 不能創(chuàng)建一個抽象類的實例
department = new AccountingDepartment() // 允許對一個抽象子類進(jìn)行實例化和賦值
department.printName()
department.printMeeting()
department.generateReports() // 錯誤: 方法在聲明的抽象類中不存在

高級技巧

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

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

class Greeter {
  static standardGreeting = 'Hello, there'
  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 類的實例的類型是 Greeter。 這對于用過其它面向?qū)ο笳Z言的程序員來講已經(jīng)是老習(xí)慣了。

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

var Greeter = /** @class */ (function () {
  function Greeter(message) {
    this.greeting = message;
  }
  Greeter.prototype.greet = function () {
    return 'Hello, ' + this.greeting;
  };
  Greeter.standardGreeting = 'Hello, there';
  return Greeter;
}());
var greeter;
greeter = new Greeter('world');
console.log(greeter.greet());

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

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

class Greeter {
  static standardGreeting = 'Hello, there'
  
  greeting: string

  constructor(message?: string) {
    this.greeting = message
  }

  greet() {
    if (this.greeting) {
      return 'Hello, ' + this.greeting
    } else {
      return Greeter.standardGreeting
    }
  }
}

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

let greeterMaker: typeof Greeter = Greeter
greeterMaker.standardGreeting = 'Hey there'

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

這個例子里, greeter1 與之前看到的一樣。 我們實例化 Greeter類,并使用這個對象。 與我們之前看到的一樣。

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

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

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

class Point {
  x: number
  y: number
}

interface Point3d extends Point {
  z: number
}

let point3d: Point3d = {x: 1, y: 2, z: 3}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容