TypeScript 面向對象編程

一、類

面向對象編程中一個重要的核心就是:,當我們使用面向對象的方式進行編程的時候,通常會首先去分析具體要實現的功能,把特性相似的抽象成一個一個的類,然后通過這些類實例化出來的具體對象來完成具體業務需求。

【1】類的基礎

在類的基礎中,包含下面幾個核心的知識點,也是 TypeScriptEMCAScript2015+在類方面共有的一些特性

  • class 關鍵字
  • 構造函數:constructor
  • 成員屬性定義
  • 成員方法
  • this關鍵字

除了以上的共同特性以外,在 TypeScript 中還有許多 ECMAScript 沒有的,或當前還不支持的一些特性,如:抽象

【2】class關鍵字

通過 class 就可以描述和組織一個類的結構,語法:

// 通常類的名稱我們會使用 大坨峰命名 規則,也就是 (單詞)首字母大寫
class User {
  // 類的特征都定義在 {} 內部
}

【3】構造函數

通過 class 定義了一個類以后,我們可以通過 new 關鍵字來調用該類從而得到該類型的一個具體對象:也就是實例化。為什么類可以像函數一樣去調用呢,其實我們執行的并不是這個類,而是類中包含的一個特殊函數:構造函數 - constructor

class User {
    constructor() {
    console.log('實例化...')
  }

}
let user1 = new User;

  • 默認情況下,構造函數是一個空函數

  • 構造函數會在類被實例化的時候調用

  • 我們定義的構造函數會覆蓋默認構造函數

  • 如果在實例化(new)一個類的時候無需傳入參數,則可以省略 ()

  • 構造函數 constructor 不允許有return 和返回值類型標注的(因為要返回實例對象)

通常情況下,我們會把一個類實例化的時候的初始化相關代碼寫在構造函數中,比如對類成員屬性的初始化賦值

【4】成員屬性與方法定義

class User {
  id: number;
  username: string;

  constructor(id: number, username: string) {
    this.id = id;
    this.username = username;
  }

  postArticle(title: string, content: string): void {
    console.log(`發表了一篇文章: ${title}`)
  }
}

let user1 = new User(1, 'Demi');
let user2 = new User(2, 'Davi');

【5】this 關鍵字

在類內部,我們可以通過 this 關鍵字來訪問類的成員屬性和方法

class User {
  id: number;
  username: string;

  postArticle(title: string, content: string): void {
    // 在類的內部可以通過 `this` 來訪問成員屬性和方法
    console.log(`${this.username} 發表了一篇文章: ${title}`)
  }
}

【6】構造函數參數屬性

因為在構造函數中對類成員屬性進行傳參賦值初始化是一個比較常見的場景,所以 ts 提供了一個簡化操作:給構造函數參數添加修飾符來直接生成成員屬性

  • public 就是類的默認修飾符,表示該成員可以在任何地方進行讀寫操作
class User {

  constructor(
    public id: number,
    public username: string
  ) {
    // 可以省略初始化賦值
  }

  postArticle(title: string, content: string): void {
    console.log(`${this.username} 發表了一篇文章: ${title}`)
  }
}

let user1 = new User(1, 'Demi');
let user2 = new User(2, 'Davi');

二、繼承

ts 中,也是通過 extends 關鍵字來實現類的繼承

class VIP extends User {

}

【1】super 關鍵字

在子類中,我們可以通過 super 來引用父類

  • 如果子類沒有重寫構造函數,則會在默認的 constructor 中調用 super()

  • 如果子類有自己的構造函數,則需要在子類構造函數中顯示的調用父類構造函數 : super(//參數),否則會報錯

  • 在子類構造函數中只有在 super(//參數) 之后才能訪問 this

  • 在子類中,可以通過 super 來訪問父類的成員屬性和方法

  • 通過 super 訪問父類的的同時,會自動綁定上下文對象為當前子類 this

class VIP extends User {

  constructor(
      id: number,
      username: string,
      public score = 0
    ) {
        super(id, username);
    }

  postAttachment(file: string): void {
    console.log(`${this.username} 上傳了一個附件: ${file}`)
  }
}

let vip1 = new VIP(1, 'Demi');
vip1.postArticle('標題', '內容');
vip1.postAttachment('1.png');

【2】方法的重寫與重載

默認情況下,子類成員方法集成自父類,但是子類也可以對它們進行重寫和重載

class VIP extends User {

    constructor(
      id: number,
      username: string,
      public score = 0
    ) {
        super(id, username);
    }

    // postArticle 方法重寫,覆蓋
    postArticle(title: string, content: string): void {
      this.score++;
      console.log(`${this.username} 發表了一篇文章: ${title},積分:${this.score}`);
    }

    postAttachment(file: string): void {
        console.log(`${this.username} 上傳了一個附件: ${file}`)
    }
}

// 具體使用場景
let vip1 = new VIP(1, 'Demi');
vip1.postArticle('標題', '內容');

class VIP extends User {

    constructor(
      id: number,
      username: string,
      public score = 0
    ) {
        super(id, username);
    }

    // 參數個數,參數類型不同:重載
    postArticle(title: string, content: string): void;
    postArticle(title: string, content: string, file: string): void;
    postArticle(title: string, content: string, file?: string) {
        super.postArticle(title, content);

        if (file) {
            this.postAttachment(file);
        }
    }

    postAttachment(file: string): void {
        console.log(`${this.username} 上傳了一個附件: ${file}`)
    }
}

// 具體使用場景
let vip1 = new VIP(1, 'Demi');
vip1.postArticle('標題', '內容');
vip1.postArticle('標題', '內容', '1.png');

三、修飾符

有的時候,我們希望對類成員(屬性、方法)進行一定的訪問控制,來保證數據的安全,通過 類修飾符 可以做到這一點,目前 TypeScript 提供了四種修飾符:

  • public:公有,默認
  • protected:受保護
  • private:私有
  • readonly:只讀

【1】public 修飾符

這個是類成員的默認修飾符,它的訪問級別為:

  • 自身
  • 子類
  • 類外

【2】protected 修飾符

它的訪問級別為:

  • 自身
  • 子類

【3】private 修飾符

它的訪問級別為:

  • 自身

【4】readonly 修飾符

只讀修飾符只能針對成員屬性使用,且必須在聲明時或構造函數里被初始化,它的訪問級別為:

  • 自身
  • 子類
  • 類外
class User {

  constructor(
    // 可以訪問,但是一旦確定不能修改
    readonly id: number,

    // 可以訪問,但是不能外部修改
    protected username: string,

    // 外部包括子類不能訪問,也不可修改
    private password: string
  ) {
    // ...
  }
    // ...
}

let user1 = new User(1, 'Demi', '123456');

四、寄存器

有的時候,我們需要對類成員 屬性 進行更加細膩的控制,就可以使用 寄存器 來完成這個需求,通過 寄存器,我們可以對類成員屬性的訪問進行攔截并加以控制,更好的控制成員屬性的設置和訪問邊界,寄存器分為兩種:

  • getter
  • setter

【1】getter

訪問控制器,當訪問指定成員屬性時調用

【2】setter- 組件

  • 函數式組件

  • 類式組件

  • props 與 state

  • 組件通信

  • 表單與受控組件

設置控制器,當設置指定成員屬性時調用

class User {

    constructor(
      readonly _id: number,
      readonly _username: string,
      private _password: string
    ) {
    }

    public set password(password: string) {
        if (password.length >= 6) {
            this._password = password;
        }
    }

    public get password() {
        return '******';
    }
    // ...
}

五、靜態成員

前面我們說到的是成員屬性和方法都是實例對象的,但是有的時候,我們需要給類本身添加成員,區分某成員是靜態還是實例的:

  • 該成員屬性或方法類型的特征還是實例化對象的特征
  • 如果一個成員方法中沒有使用或依賴 this ,那么該方法就是靜態的
type IAllowFileTypeList = 'png'|'gif'|'jpg'|'jpeg'|'webp';

class VIP extends User {

  // static 必須在 readonly 之前
  static readonly ALLOW_FILE_TYPE_LIST: Array<IAllowFileTypeList> = ['png','gif','jpg','jpeg','webp'];

  constructor(
      id: number,
      username: string,
      private _allowFileTypes: Array<IAllowFileTypeList>
    ) {
        super(id, username);
  }

  info(): void {
    // 類的靜態成員都是使用 類名.靜態成員 來訪問
    // VIP 這種類型的用戶允許上傳的所有類型有哪一些
    console.log(VIP.ALLOW_FILE_TYPE_LIST);
    // 當前這個 vip 用戶允許上傳類型有哪一些
    console.log(this._allowFileTypes);
  }
}

let vip1 = new VIP(1, 'Demi', ['jpg','jpeg']);
// 類的靜態成員都是使用 類名.靜態成員 來訪問
console.log(VIP.ALLOW_FILE_TYPE_LIST);
this.info();

  • 類的靜態成員是屬于類的,所以不能通過實例對象(包括 this)來進行訪問,而是直接通過類名訪問(不管是類內還是類外)
  • 靜態成員也可以通過訪問修飾符進行修飾
  • 靜態成員屬性一般約定(非規定)全大寫

六、抽象類

有的時候,一個基類(父類)的一些方法無法確定具體的行為,而是由繼承的子類去實現,看下面的例子:

現在前端比較流行組件化設計,比如 React

class MyComponent extends Component {

  constructor(props) {
    super(props);
    this.state = {}
  }

  render() {
    //...
  }

}

根據上面代碼,我們可以大致設計如下類結構

  • 每個組件都一個 props 屬性,可以通過構造函數進行初始化,由父級定義
  • 每個組件都一個 state 屬性,由父級定義
  • 每個組件都必須有一個 render 的方法
class Component<T1, T2> {

    public state: T2;

    constructor(
        public props: T1
    ) {
        // ...
    }

    render(): string {
        // ...不知道做點啥才好,但是為了避免子類沒有 render 方法而導致組件解析錯誤,父類就用一個默認的 render 去處理可能會出現的錯誤
    }
}

interface IMyComponentProps {
    title: string;
}
interface IMyComponentState {
    val: number;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> {

    constructor(props: IMyComponentProps) {
        super(props);

        this.state = {
            val: 1
        }
    }

    render() {
        this.props.title;
        this.state.val;
        return `<div>組件</div>`;
    }

}

上面的代碼雖然從功能上講沒什么太大問題,但是我們可以看到,父類的 render有點尷尬,其實我們更應該從代碼層面上去約束子類必須得有 render 方法,否則編碼就不能通過

【1】abstract 關鍵字

如果一個方法沒有具體的實現方法,則可以通過 abstract 關鍵字進行修飾

abstract class Component<T1, T2> {

    public state: T2;

    constructor(
        public props: T1
    ) {
    }

    public abstract render(): string;
}

使用抽象類有一個好處:

約定了所有繼承子類的所必須實現的方法,使類的設計更加的規范

使用注意事項:

  • abstract 修飾的方法不能有方法體
  • 如果一個類有抽象方法,那么該類也必須為抽象的
  • 如果一個類是抽象的,那么就不能使用 new 進行實例化(因為抽象類表名該類有未實現的方法,所以不允許實例化)
  • 如果一個子類繼承了一個抽象類,那么該子類就必須實現抽象類中的所有抽象方法,否則該類還得聲明為抽象的

七、類與接口

在前面我們已經學習了接口的使用,通過接口,我們可以為對象定義一種結構和契約。我們還可以把接口與類進行結合,通過接口,讓類去強制符合某種契約,從某個方面來說,當一個抽象類中只有抽象的時候,它就與接口沒有太大區別了,這個時候,我們更推薦通過接口的方式來定義契約

  • 抽象類編譯后還是會產生實體代碼,而接口不會
  • TypeScript 只支持單繼承,即一個子類只能有一個父類,但是一個類可以實現過個接口
  • 接口不能有實現,抽象類可以

【1】implements

在一個類中使用接口并不是使用 extends 關鍵字,而是 implements

  • 與接口類似,如果一個類 implements 了一個接口,那么就必須實現該接口中定義的契約
  • 多個接口使用 , 分隔
  • implementsextends 可同時存在
interface ILog {
  getInfo(): string;
}

class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog {

    constructor(props: IMyComponentProps) {
        super(props);

        this.state = {
            val: 1
        }
    }

    render() {
        this.props.title;
        this.state.val;
        return `<div>組件</div>`;
    }

    getInfo() {
        return `組件:MyComponent,props:${this.props},state:${this.state}`;
    }

}

實現多個接口

interface ILog {
    getInfo(): string;
}
interface IStorage {
    save(data: string): void;
}

class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog, IStorage {

    constructor(props: IMyComponentProps) {
        super(props);

        this.state = {
            val: 1
        }
    }

    render() {
        this.props.title;
        this.state.val;
        return `<div>組件</div>`;
    }

    getInfo(): string {
        return `組件:MyComponent,props:${this.props},state:${this.state}`;
    }

    save(data: string) {
        // ... 存儲
    }

}

接口也可以繼承

interface ILog {
    getInfo(): string;
}
interface IStorage extends ILog {
    save(data: string): void;
}

八、類與對象類型

當我們在 TypeScript 定義一個類的時候,其實同時定義了兩個不同的類型

  • 類類型(構造函數類型)
  • 對象類型

首先,對象類型好理解,就是我們的 new 出來的實例類型

那類類型是什么,我們知道 JavaScript 中的類,或者說是 TypeScript 中的類其實本質上還是一個函數,當然我們也稱為構造函數,那么這個類或者構造函數本身也是有類型的,那么這個類型就是類的類型

class Person {
    // 屬于類的
  static type = '人';

  // 屬于實例的
  name: string;
  age: number;
  gender: string;

  // 類的構造函數也是屬于類的
  constructor( name: string, age: number, gender: '男'|'女' = '男' ) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }

  public eat(): void {
    // ...
  }

}

let p1 = new Person('Demi', 24, '女');
p1.eat();
Person.type;

上面例子中,有兩個不同的數據

  • Person 類(構造函數)
  • 通過 Person 實例化出來的對象 p1

對應的也有兩種不同的類型

  • 實例的類型(Person
  • 構造函數的類型(typeof Person

用接口的方式描述如下

interface Person {
    name: string;
    age: number;
    gender: string;
    eat(): void;
}

interface PersonConstructor {
    // new 表示它是一個構造函數
    new (name: string, age: number, gender: '男'|'女'): PersonInstance;
    type: string;
}

在使用的時候要格外注意

function fn1(arg: Person /*如果希望這里傳入的Person 的實例對象*/) {
    arg.eat();
}
fn1( new Person('', 1, '男') );

function fn2(arg: typeof Person /*如果希望傳入的Person構造函數*/) {
    new arg('', 1, '男');
}
fn2(Person);

文章每周持續更新,可以微信搜索「 前端大集錦 」第一時間閱讀,回復【視頻】【書籍】領取200G視頻資料和30本PDF書籍資料

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容