在面向對象(OOP)編程中,經常會使用到class(類)和interface(接口)。在TypeScript(以下簡稱TS)中也引入了類和接口的概念,使得TS強大的類型檢測機制更加完善。就像我們所知道的,一個類是一堆抽象概念的集合,我們可以從類的構造方法中創建出享有共同屬性和方法的對象。一個接口所描述的是一個對象相關的屬性和方法,但並不提供具體創建此對象實例的方法。
我們的前端項目使用Angular2.0+作為技術棧,Angular2.0+基於TS實現,我們在對代碼中某些部分添加類型注釋的時,經常出現會面對這樣的問題:
"我應該使用接口還是類來對當前數據進行類型注釋?
一個例子
在開發過程中經常會有這樣的操作
fetch('https://xxx.com/api/blabla') .then(data => { console.log(data); // data: any });
以上代碼片段向后端API發起請求,得到返回的數據后我們會在前端UI中使用。
如果我們讓TS編譯目前這段代碼,then方法中的data參數將會默認被定義為any類型。因為在沒有類型定義的情況下,TS僅僅是通過分析代碼,判斷出類型應該是什么。
在這種情況下,我們為了提高程序的類型安全性,我們希望能夠主動地添加一個類型注釋`Response`,以便告知TS編譯器我們期望當前res參數的類型為什么。
// 需要在此處定義一個 `Response` 類型
fetch('https://xxx.com/api/blabla') .then((data: Response) => { console.log(data) // data: Response })
由此,我們引出了本篇文章的核心問題,我們應該把`Response`類型定義為interface還是一個class呢?總感覺class和interface都可以。
TS中的interface
TS的核心原則職之一就是類型檢查,關注定義的值的數據結構
interface是僅存在於TS上下文中的一種虛擬結構,TS編譯器依賴接口用於類型檢查,最終編譯為JS后,接口將會被移除。
interface MyInterface { a: number; b: string; }
以上MyInterface這個接口約定對象只能存在且必須有兩個屬性,這兩個屬性分別為數字 `a` 和 字符串 `b`,只要不遵守此約定,TS就會拋出錯誤。
TS中的class
與其他語言相比,JS並沒有直接對類的描述,基於原型的繼承方式也讓眾多的OOP世界的程序員充滿困惑,一直到了ES6,class關鍵字作為一種語法糖出現。
與interface不同,class作為TS的一種變量類型存在於上下文之中,class中可以提供,變量、方法等的具體實現方式等,它的作用不僅僅是約束數據結構。
class MyClass { a: number; b: string; constructor(options: MyInterface) { this.a = options.a; this.b = options.b; } foo(): void { console.log(this.a); console.log(this.b); } }
以上MyClass類定義了兩個變量和一個foo方法,constructor方法會接受options參數初始化類中的屬性。
Class和Interface的比較
在TS中class和interface都可以用來約束數據的結構,但是頻繁使用class約束數據結構會使程序的性能受到影響,在 [typescript官網](https://www.tslang.cn/play/index.html) 的練習板塊中,我們在左邊書寫TS代碼,右邊會顯示所轉換成的JS代碼。
我們嘗試書寫class和interface看看兩者轉換后的代我們可以發現class編譯了大量代碼,但是interface並沒有轉換成任何JS,當我們定義大量的class,並且還有着復雜的繼承關系時,編譯過后的代碼體積將更加龐大。
最佳實踐
由於考慮到class和interface在TS中編譯結果的不同,我們面對不同的場景,使用正確的約束數據類型的方式,對我們代碼性能層面的提高就尤為重要。
什么時候使用class
當需要使用class時,我通常會考慮三個方面
- 是否需要創建多個實例
- 是否需要使用繼承
- 是否需要特定的單例對象
什么時候使用interface
對於從服務器端獲取或者業務場景中模擬的數據,提倡使用interface去定義,這些數據通常是不會經常變化和調整的,這些數據可能僅僅只表示某些狀態,或者是UI上的文本。
interface配合class
class MyClass { a: number; b: string; origin: MyInterface; constructor(options: MyInterface) { this.a = options.a; this.b = options.b; this.origin = options; // 保存原始的options數據 } foo(): void { console.log(this.a); console.log(this.b); } } const a = new MyClass(options); const b = new MyClass(a.origin);
在實際場景中,我們可以給class的參數指定好interface類型用來初始化class中的屬性,以上代碼在class的origin屬性中保存了options的數據,可以用來之后初始化全新的class實例,這解決了class實例在變化后很難clone出全新實例的問題,上面實例中 `b`變量 由 `a`變量 的orgin字段
初始化。
總之,當我們只是想要在TS中約束數據類型時,我們需要結合實際的場景去選擇使用class還是interface,如果只是一個簡單的后端請求,卻都使用class去約束,想象一下編譯出來一大堆無用的js代碼,簡直可怕ヽ(*。>Д<)o゜