TypeScript入門 - 6

by vivid 6. 三月 2019 03:56

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N190320501
出刊日期: 2019/3/6

TypeScript中的類別(Class)類似C#、Java程式語言中的類別,可以包含函式(function)與變數(Variable),方法(function)與變數(Variable)可以套用「public」、「private」等等修飾詞來控制可視性。TypeScript類別可以繼承一個類別(Class),並實作多個介面(Interface)。本篇文章將延續《TypeScript入門》系列文章,介紹類別的繼承的語法,以及了解介面,如何在類別之中實作介面。

繼承(Inheritance)

在TypeScript中類別(class)可以使用「extends」關鍵字來繼承其它類別,以達到程式碼的重複使用性。被繼承的類別稱父類別(Parent Class,或稱基礎類別);實作繼承的類別稱子類別(Child Class,或稱衍生類別)。類別只要實作了繼承功能,子類別就可以擁有和父類別名稱相同的屬性,並可直接叫用定義在父類別的方法。為了避免設計上的困擾,在TypeScript中一個類別只能繼承一個父類別,實作單一繼承(single inheritance),不可做多重繼承(Multiple inheritance),不過TypeScript具有介面(Interface),可以模擬出多重繼承的效果,這個部分稍後再敘,我們先來看看繼承的語法。

參考以下範例程式碼,展示最簡單的繼承功能,「Employee」類別包含一個建構函式與一個「getInfo」方法。利用「private」修飾詞,在「Employee」類別之中定義「empId」與「empName」兩個屬性。在「Sales」類別利用「extends」關鍵字繼承「Employee」類別,因此「Sales」物件便可以直接叫用「getInfo」方法:

class Employee {
  constructor( private empId: number, private empName: string ) {
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName}`;
  }
}
class Sales extends Employee {
}

let s1 = new Sales( 1, "mary" );
console.log( s1.getInfo() ); // 1 , mary

 

若使用「tsc」工具程式將其轉換成JavaScript語法:

tsc myts.ts

轉換完成的程式碼參考如下:

var __extends = ( this && this.__extends ) || ( function () {
    var extendStatics = function ( d, b ) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if ( b.hasOwnProperty(p )) d[p] = b[p]; };
        return extendStatics( d, b );
    }
    return function ( d, b ) {
        extendStatics( d, b );
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Employee = /** @class */ (function () {
    function Employee( empId, empName ) {
        this.empId = empId;
        this.empName = empName;
    }
    Employee.prototype.getInfo = function () {
        return " " + this.empId + " , " + this.empName;
    };
    return Employee;
}());
var Sales = /** @class */ (function ( _super ) {
    __extends( Sales, _super );
    function Sales() {
        return _super !== null && _super.apply( this, arguments ) || this;
    }
    return Sales;
}( Employee ));
var s1 = new Sales( 1, "mary" );
console.log( s1.getInfo() ); // 1 , mary

特別注意,子類別「Sales」的建構函式第一行必需呼叫「super」方法以初始化「this」,建構函式若沒有叫用「super」則無法通過語法檢查,錯誤訊息請參考下圖所示:

clip_image002

圖 1:子類別的建構函式必需呼叫「super」方法以初始化「this」。

參考以下範例程式碼,「Sales」子類別的建構函式第一行必需呼叫「super」方法以初始化「this」:

class Employee {
  constructor( private empId: number, private empName: string ) {
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName}`;
  }
}

class Sales extends Employee {
  constructor( empId: number, empName: string, private bonus: number ) {
    super( empId, empName );
  }
}

let s1 = new Sales( 1, "mary", 1000 );
console.log( s1.getInfo() ); // 1 , mary

 

使用「tsc」工具程式將其轉換成JavaScript程式碼參考如下:

var __extends = ( this && this.__extends ) || ( function () {
    var extendStatics = function ( d, b ) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function ( d, b ) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if ( b.hasOwnProperty(p) ) d[p] = b[p]; };
        return extendStatics( d, b );
    }
    return function ( d, b ) {
        extendStatics( d, b );
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : ( __.prototype = b.prototype, new __() );
    };
})();
var Employee = /** @class */ (function () {
    function Employee( empId, empName ) {
        this.empId = empId;
        this.empName = empName;
    }
    Employee.prototype.getInfo = function () {
        return " " + this.empId + " , " + this.empName;
    };
    return Employee;
}());
var Sales = /** @class */ (function ( _super ) {
    __extends( Sales, _super );
    function Sales( empId, empName, bonus ) {
        var _this = _super.call( this, empId, empName ) || this;
        _this.bonus = bonus;
        return _this;
    }
    return Sales;
}( Employee ));
var s1 = new Sales( 1, "mary", 1000 );
console.log( s1.getInfo() ); // 1 , mary

 

在子類別建構函式叫用「super」方法設定「this」之後,後續的程式碼才可以使用「this」關鍵字來存取物件的屬性或方法,參考以下範例程式碼,在「Sales」類別中,新增一個「bonus」屬性,接著使用「this」關鍵字來存取「bonus」屬性,以及叫用「Employee」類別的「getInfo」方法:

class Employee {
  constructor( private empId: number, private empName: string ) {
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName}`;
  }
}
class Sales extends Employee {
  constructor( empId: number, empName: string, private bonus: number ) {
    super( empId, empName );
    console.log( this.bonus ); // 1000
    console.log( this.getInfo() ) // 1 , mary
  }
}
let s1 = new Sales( 1, "mary", 1000 );
console.log( s1.getInfo() ); // 1 , mary

特別注意,在「Sales」類別的建構函式中,我們移除了「empId」與「empName」參數前方的「private」修飾詞,因為「Employee」類別已定義同名的屬性,「Sales」子類別中不需要重複定義,否則將會發生語法錯誤,請參考下圖所示:

clip_image004

圖 2:繼承錯誤。

此外「Sales」類別的建構函式中額外定義一個「bonus」參數,因為有加上「private」修飾詞的關係,它將為「Sales」類別定義一個屬性。上例程式碼可以改寫如下,在「Sales」類別中明確定義「bouns」屬性,並移除建構函式中「bonus」參數前方的「private」修飾詞:

class Employee {
  constructor( private empId: number, private empName: string ) {
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName}`;
  }
}
class Sales extends Employee {
  bonus : number;
  constructor( empId: number, empName: string,  bonus: number ) {
    super( empId, empName );
    this.bonus = bonus;
    console.log( this.bonus ); // 1000
    console.log( this.getInfo() ) // 1 , mary
  }
}

let s1 = new Sales( 1, "mary", 1000 );
console.log( s1.getInfo() ); // 1 , mary

 

 

方法改寫 (Method Overwrite)

若繼承下來的方法不敷使用,子類別可以改寫繼承下來的方法,此動作稱為方法改寫 (Method Overwrite),這是達到物件導向程式語言多形(Polymorphism)的一種手段。我們只要在子類別中定義一個和父類別同名的方法即可。

參考以下範例程式碼,「Sales」子類別定義一個和「Employee」父類別同名的「getInfo」方法,然後改寫「Employee」父類別的「getInfo」方法。在「getInfo」方法中可以使用「this」存取「Employee」父類別的「empId」、「empName」屬性,但前題是,父類別的屬性需要前置「public」或「protected」關鍵字:

class Employee {
  constructor( public empId: number, public empName: string ) {
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName} `;
  }
}

class Sales extends Employee {
  constructor( empId: number, empName: string, private bonus: number ) {
    super( empId, empName );
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName} , ${this.bonus} `;
  }
}

let s1 = new Sales( 1, "mary", 1000 );
console.log( s1.getInfo() ); //  1 , mary , 1000

let e1 = new Employee( 2, "candy" );
console.log( e1.empId ); // 2
console.log( e1.empName ); // candy

 

 

「Employee」父類別宣告為「protected」或「public」的成員,在「Sales」子類別中可以直接存取。但沒有繼承關係的外部程式碼則無法存取。參考以下範例程式碼是使用「protected」範例:

class Employee {
  constructor( protected empId: number, protected empName: string ) {
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName} `;
  }
}

class Sales extends Employee {
  constructor( empId: number,  empName: string, private bonus: number ) {
    super( empId, empName );
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName} , ${this.bonus} `;
  }
}

let s1 = new Sales( 1, "mary", 1000 );
console.log( s1.getInfo() ); //  1 , mary , 1000

console.log( e1.empId ); // error
console.log( e1.empName ); // error

 

而在「Sales」子類別的方法之中,若要叫用到定義在「Employee」父類別的方法,則一樣可以使用「super」關鍵字,例如修改上例程式碼如下,「Sales」類別的「getInfo」方法中先叫用「Employee」類別的「getInfo」方法取得「empId」與「empName」資訊,再做字串串接「bonus」的資料後回傳:

class Employee {
  constructor( public empId: number, public empName: string ) {
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName} `;
  }
}

class Sales extends Employee {
  constructor( empId: number, empName: string, private bonus: number ) {
    super( empId, empName );
  }
  getInfo() {
    return super.getInfo() + ` , ${this.bonus} `;
  }
}

let s1 = new Sales( 1, "mary", 1000 );
console.log( s1.getInfo() ); //  1 , mary , 1000

let e1 = new Employee( 2, "candy" );
console.log( e1.empId ); // 2
console.log( e1.empName ); // candy

 

protected修飾詞

建構函式可以標識為「protected」,這表示你只能夠在其子類別中呼叫它們,不可以在外部程式碼中叫用之。舉例來說,參考以下範例程式碼,在「Sales」類別的建構函式中,可以使用「super」叫用「Employee」類別的建構函式,但最後一行程式將會發生錯誤,你無法使用「new」關鍵字,直接叫用「Employee」類別的建構函式:

class Employee {
protected constructor( public empId: number, public empName: string ) {
  }
  getInfo() {
    return ` ${this.empId} , ${this.empName} `;
  }
}

class Sales extends Employee {
  constructor( empId: number, empName: string, private bonus: number ) {
    super( empId, empName );
  }
  getInfo() {
    return super.getInfo() + ` , ${this.bonus} `;
  }
}

let s1 = new Sales( 1, "mary", 1000 );
console.log( s1.getInfo() ); //  1 , mary , 1000

let e1 = new Employee( 2, "candy" ); //error

 

介面(Interface)

以往我們透過「鴨子型別(duck typing)」的寫作風格來判斷物件的型別,若物件具備特定的屬性或方法,那麼他就是特定型別的物件。TypeScript擁有絕佳型別檢查的功能,現在透過介面(Interface)可以讓你為型別命名,同時介面也可以讓你為你的程式碼與外部程式之間定義一個共通的合約。

我們利用一個例子來看看鴨子型別(duck typing)的用法,參考以下範例程式碼,定義了一個「SayHello」函式,此函式有一個物件型別的參數,物件包含了字串型別的「msg」屬性:

function SayHello( obj: { msg: string } ) {
  console.log( obj.msg );
}

let o1 = { msg: "Hello World" };
SayHello( o1 ); // Hello World
let o2 = { text: "Hello World", msg: "Hello World" };
SayHello( o2 ); // Hello World
let o3 = { text: "Hello World" };
//SayHello( o3 ); //error TS2345: Argument of type '{ text: string; }' is not assignable to parameter of type '{ msg: string; }'. Property 'msg' is missing in type '{ text: string; }'.

 

TypeScript將會檢查叫用「SayHello」函式時,是否傳入一個具備有「msg」屬性的物件,如「o1」、「o2」,若有包函此屬性,則可正確叫用「SayHello」函式;而「o2」並沒有具備「msg」屬性,因此將會發生型別錯誤。

接著我們來看一下介面的用法,參考以下範例程式碼,使用「interface」關鍵字定義一個名為「HelloMsg」的介面,包含一個字串型別的「msg」屬性:

interface HelloMsg {
  msg: string
}

function SayHello( obj: HelloMsg ) {
  console.log( obj.msg );
}

let o1: HelloMsg = { msg: "Hello World" };
SayHello( o1 ); // Hello World
let o2: HelloMsg = { text: "Hello World", msg: "Hello World" }; // error TS2322: Type '{ text: string; msg: string; }' is not assignable to type 'HelloMsg'.
let o3: HelloMsg = { text: "Hello World" }; //error TS2322: Type '{ text: string; }' is not assignable to type 'HelloMsg'.

定義「SayHello」函式時,我們便可用「HelloMsg」這個名稱來描述「SayHello」函式需要接收一個具備「msg」屬性的參數物件。同樣的範例中的「o1」具備有此屬性,因此「SayHello(o1)」」呼叫的動作成功地執行,而與「SayHello(o2)」、「SayHello(o3)」則因型別檢查不通過無法執行,發生錯誤。最後要提醒的是:TypeScript介面和C#、Java程式語言不一樣的地方在於,「o1」並不需要實作「HelloMsg」介面。

 

選擇性屬性

在介面中的屬性不一定是必要的,可以是選擇性的。舉例來說,修改上例程式碼,「HelloMsg」介面中的「msg」屬性名稱後方加上「?」符號,表示此為選擇性屬性,因此以下範例程式碼中「SayHello(o1)」這一行程式可以正確的叫用並執行:

interface HelloMsg {
  msg?: string;
}

function SayHello( obj: HelloMsg ) {
  console.log( obj.msg );
}

let o1: HelloMsg = {};
SayHello( o1 ); // undefined
let o2: HelloMsg = { text: "Hello World", msg: "Hello World" };
SayHello( o2 ); //  error TS2322: Type '{ text: string; msg: string; }' is not assignable to type 'HelloMsg'.
let o3: HelloMsg = { text: "Hello World" };
SayHello( o3 ); // error TS2322: Type '{ text: string; }' is not assignable to type 'HelloMsg'.

 

使用選擇性屬性的好處是,你可以檢查傳入「SayHello」函式的物件參數是否同樣具備了「msg」屬性,也可以避免你傳入的物件參數不包含介面中的屬性,例如上面範例中的「o2」、「o3」將會發生型別錯誤,錯誤資訊請參考下圖所示:

clip_image006

圖 3:型別檢查錯誤。

不過你要小心程式碼的撰寫方式,參考以下範例程式碼,如果這樣撰寫程式碼,宣告「o2」時不指定型別,那麼可能會得到意外的驚奇,TypeScript將不會檢查參數物件是否多包含沒有在介面定義的屬性:

interface HelloMsg {
  msg: string
}

function SayHello( obj: HelloMsg ) {
  console.log( obj.msg );
}

let o2 = { text: "Hello World", msg: "Hello World" };
SayHello( o2 ); // Hello World

 

在介面定義函式

除了可以在介面定義屬性之外,也可以在介面中定義函式(function)。如此便可以檢查叫用函式時,函式的參數列與回傳值型別是否符合需求。參考以下範例程式碼,「IGreeting」介面定義一個函式,需要有一個字串型別的傳入參數,沒有回傳值。「SayHello」有滿足需求,可以正確叫用,而「SayHello2」則會發生型別錯誤:

interface IGreeting {
  ( msg: string ): void;
}

let SayHello: IGreeting = function ( msg: string ) {
  console.log( msg );
}

SayHello( "Hello World" );

let SayHello2: IGreeting = function ( msg: number ) {   // error TS2322: Type '(msg: number) => void' is not assignable to type 'IGreeting'.
  console.log( msg );
}

 

在類別實作介面(Interface)

介面(Interface)之中可以定義屬性與方法,TypeScript和Java、C#程式語言一樣,可以利用介面來定義你的類別需符合特定的合約,以檢查類別是否有包含介面中定義的屬性與方法。

類別若要實作介面,可以使用「implements」關鍵字。參考以下範例程式碼,「IShape」介面中包含「width」、「height」兩個屬性,以及一個「getArea」方法:

interface IShape {
  width: number;
  height: number;
  getArea(): number;
}

class Rectangle implements IShape {
  width: number;
  height: number;
  constructor( w: number, h: number ) {
    this.width = w;
    this.height = h;
  }
  getArea() {
    return this.width * this.height;
  }
}

let s = new Rectangle( 10,20 );
console.log( s.getArea() ) // 200

 

「Rectangle」類別則實作了「IShape」介面,定義了「width」、「height」兩個屬性,並實作了「getArea」方法程式碼,因此程式可以順利執行。若「Rectangle」類別未實作定義在介面的屬性與方法,例如參考以下範例程式碼,當註解掉「height」屬性的定義就會產生語法錯誤:

class Rectangle implements IShape { //error TS2420: Class 'Rectangle' incorrectly implements interface 'IShape'. Property 'height' is missing in type 'Rectangle'.
  width: number;
  //height: number;
  constructor( w: number, h: number ) {
    this.width = w;
    //this.height = h;
  }
  // getArea() {
  //   return this.width * this.height;
  // }
}

 

型別檢查錯誤的資訊請參考下圖所示:

clip_image008

圖 4:型別檢查錯誤。

實作多重介面

一個類別可以實作多個介面,只要在「implements」關鍵字之後,使用「,」號區隔介面名稱,然後在類別中實作所有介面中的屬性與方法即可,參考以下範例程式碼,定義「ISize」與「IArea」兩個介面,而「Rectangle」類別則實作了這兩個介面:

interface ISize {
  width: number;
  height: number;
}

interface IArea {
  getArea(): number;
}

class Rectangle implements ISize, IArea {
  width: number;
  height: number;
  constructor( w: number, h: number ) {
    this.width = w;
    this.height = h;
  }
  getArea() {
    return this.width * this.height;
  }
}

let s = new Rectangle( 10, 20 );
console.log( s.width ); // 10
console.log( s.height ); // 20
console.log( s.getArea() ) // 200

任一個介面定義的功能未在「Rectangle」類別實作就會發生語法錯誤,例如只要將「Rectangle」類別「getArea」函式的程式註解,就會看到錯誤訊息。

clip_image010

圖 5:未實作介面所有功能。

介面繼承

在TypeScript之中,介面和類別類似,可以使用「extends」關鍵字來繼承其他介面,和類別不一樣的地方在於,介面可以繼承多重介面。如此可以讓程式設計師不用了解太多小介面的細節,將多個小介面組成一個大介面,可以簡化介面的使用。

參考以下範例程式碼,「IShape」介面繼承了「ISize」與「IArea」兩個介面,對於「Rectangle」類別而言,只需要認識「IShape」介面,不需了解「ISize」與「IArea」兩個介面的細節,只要實作定義在這兩個介面中的所有屬性與方法即可,若有部分屬性與方法沒有實作,便可檢查出語法錯誤。

interface ISize {
  width: number;
  height: number;
}

interface IArea {
  getArea(): number;
}
interface IShape extends ISize, IArea {
}

class Rectangle implements IShape {
  width: number;
  height: number;
  constructor( w: number, h: number ) {
    this.width = w;
    this.height = h;
  }
  getArea() {
    return this.width * this.height;
  }
}

let s = new Rectangle( 10, 20 );
console.log( s.width ); // 10
console.log( s.height ); // 20
console.log( s.getArea() ) // 200

Tags:

.NET Magazine國際中文電子雜誌 | JavaScript | TypeScript

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List