TS 類這十個知識點,你都掌握了么?
在面向對象語言中,類是一種面向對象計算機編程語言的構造,是創(chuàng)建對象的藍圖,描述了所創(chuàng)建的對象共同的屬性和方法。本文阿寶哥將跟大家一起學習一下 TS 類涉及的十個知識點。
一、類的屬性與方法
1.1 類的成員屬性和靜態(tài)屬性
在 TypeScript 中,我們可以通過 class 關鍵字來定義一個類:
- class Person {
- name: string; // 成員屬性
- constructor(name: string) { // 類的構造函數(shù)
- this.name = name;
- }
- }
在以上代碼中,我們使用 class 關鍵字定義了一個 Person 類,該類含有一個名為 name 的成員屬性。其實 TypeScript 中的類是一個語法糖(所謂的語法糖就是在之前的某個語法的基礎上改變了一種寫法,實現(xiàn)的功能相同,但是寫法不同了,主要是為了讓開發(fā)人員在使用過程中更方便易懂。),若設置編譯目標為 ES5 將會產生以下代碼:
- "use strict";
- var Person = /** @class */ (function () {
- function Person(name) {
- this.name = name;
- }
- return Person;
- }());
類除了可以定義成員屬性外,還可以通過 static 關鍵字定義靜態(tài)屬性:
- class Person {
- static cid: string = "exe";
- name: string; // 成員屬性
- constructor(name: string) { // 類的構造函數(shù)
- this.name = name;
- }
- }
那么成員屬性與靜態(tài)屬性有什么區(qū)別呢?在回答這個問題之前,我們先來看一下編譯生成的 ES5 代碼:
- "use strict";
- var Person = /** @class */ (function () {
- function Person(name) {
- this.name = name;
- }
- Person.cid = "exe";
- return Person;
- }());
觀察以上代碼可知,成員屬性是定義在類的實例上,而靜態(tài)屬性是定義在構造函數(shù)上。
1.2 類的成員方法和靜態(tài)方法
在 TS 類中,我們不僅可以定義成員屬性和靜態(tài)屬性,還可以定義成員方法和靜態(tài)方法,具體如下所示:
- class Person {
- static cid: string = "exe";
- name: string; // 成員屬性
- static printCid() { // 定義靜態(tài)方法
- console.log(Person.cid);
- }
- constructor(name: string) { // 類的構造函數(shù)
- this.name = name;
- }
- say(words: string) :void { // 定義成員方法
- console.log(`${this.name} says:${words}`);
- }
- }
那么成員方法與靜態(tài)方法有什么區(qū)別呢?同樣,在回答這個問題之前,我們先來看一下編譯生成的 ES5 代碼:
- "use strict";
- var Person = /** @class */ (function () {
- function Person(name) {
- this.name = name;
- }
- Person.printCid = function () {
- console.log(Person.cid);
- };
- Person.prototype.say = function (words) {
- console.log(this.name + " says\uFF1A" + words);
- };
- Person.cid = "exe";
- return Person;
- }());
由以上代碼可知,成員方法會被添加到構造函數(shù)的原型對象上,而靜態(tài)方法會被添加到構造函數(shù)上。
1.3 類成員方法重載
函數(shù)重載或方法重載是使用相同名稱和不同參數(shù)數(shù)量或類型創(chuàng)建多個方法的一種能力。 在定義類的成員方法時,我們也可以對成員方法進行重載:
- class Person {
- constructor(public name: string) {}
- say(): void;
- say(words: string): void;
- say(words?: string) :void { // 方法重載
- if(typeof words === "string") {
- console.log(`${this.name} says:${words}`);
- } else {
- console.log(`${this.name} says:Nothing`);
- }
- }
- }
- let p1 = new Person("Semlinker");
- p1.say();
- p1.say("Hello TS");
如果想進一步了解函數(shù)重載的話,可以繼續(xù)閱讀 是時候表演真正的技術了 - TS 分身之術 這一篇文章。
二、訪問器
在 TypeScript 中,我們可以通過 getter 和 setter 方法來實現(xiàn)數(shù)據(jù)的封裝和有效性校驗,防止出現(xiàn)異常數(shù)據(jù)。
- let passcode = "Hello TypeScript";
- class Employee {
- private _fullName: string = "";
- get fullName(): string {
- return this._fullName;
- }
- set fullName(newName: string) {
- if (passcode && passcode == "Hello TypeScript") {
- this._fullName = newName;
- } else {
- console.log("Error: Unauthorized update of employee!");
- }
- }
- }
- let employee = new Employee();
- employee.fullName = "Semlinker";
在以上代碼中,對于私有的 _fullName 屬性,我們通過對外提供 getter 和 setter 來控制該屬性的訪問和修改。
三、類的繼承
繼承(Inheritance)是一種聯(lián)結類與類的層次模型。指的是一個類(稱為子類、子接口)繼承另外的一個類(稱為父類、父接口)的功能,并可以增加它自己的新功能的能力,繼承是類與類或者接口與接口之間最常見的關系。通過類的繼承,我們可以實現(xiàn)代碼的復用。
繼承是一種 is-a 關系:
在 TypeScript 中,我們可以通過 extends 關鍵字來實現(xiàn)類的繼承:
3.1 父類
- class Person {
- constructor(public name: string) {}
- public say(words: string) :void {
- console.log(`${this.name} says:${words}`);
- }
- }
3.2 子類
- class Developer extends Person {
- constructor(name: string) {
- super(name);
- this.say("Learn TypeScript")
- }
- }
- const p2 = new Developer("semlinker");
- // 輸出: "semlinker says:Learn TypeScript"
因為 Developer 類繼承了 Person 類,所以我們可以在 Developer 類的構造函數(shù)中調用 say 方法。需要注意的是,在 TypeScript 中使用 extends 時,只能繼承單個類:
- class Programmer {}
- // Classes can only extend a single class.(1174)
- class Developer extends Person, Programmer {
- constructor(name: string) {
- super(name);
- this.say("Learn TypeScript")
- }
- }
雖然在 TypeScript 中只允許單繼承,但卻允許我們實現(xiàn)多個接口。具體的使用示例如下所示:
- interface CanSay {
- say(words: string) :void
- }
- interface CanWalk {
- walk(): void;
- }
- class Person implements CanSay, CanWalk {
- constructor(public name: string) {}
- public say(words: string) :void {
- console.log(`${this.name} says:${words}`);
- }
- public walk(): void {
- console.log(`${this.name} walk with feet`);
- }
- }
此外,除了可以繼承具體的實現(xiàn)類之外,在實現(xiàn)繼承時,我們還可以繼承抽象類。
四、抽象類
使用 abstract 關鍵字聲明的類,我們稱之為抽象類。抽象類不能被實例化,因為它里面包含一個或多個抽象方法。 所謂的抽象方法,是指不包含具體實現(xiàn)的方法:
- abstract class Person {
- constructor(public name: string){}
- abstract say(words: string) :void;
- }
- // Cannot create an instance of an abstract class.(2511)
- const lolo = new Person(); // Error
抽象類不能被直接實例化,我們只能實例化實現(xiàn)了所有抽象方法的子類。具體如下所示:
- class Developer extends Person {
- constructor(name: string) {
- super(name);
- }
- say(words: string): void {
- console.log(`${this.name} says ${words}`);
- }
- }
- const lolo = new Developer("lolo");
- lolo.say("I love ts!"); // 輸出:lolo says I love ts!
五、類訪問修飾
符在 TS 類型中,我們可以使用 public、protected 或 private 來描述該類屬性和方法的可見性。
5.1 public
public 修飾的屬性或者方法是公有的,可以在任何地方被訪問到,默認所有的屬性或者方法都是 public:
- class Person {
- constructor(public name: string) {}
- public say(words: string) :void {
- console.log(`${this.name} says:${words}`);
- }
- }
5.2 protected
protected 修飾的屬性或者方法是受保護的,它和 private 類似,不同的地方是 protected 成員在派生類中仍然可以訪問。
- class Person {
- constructor(public name: string) {}
- public say(words: string) :void {
- console.log(`${this.name} says:${words}`);
- }
- protected getClassName() {
- return "Person";
- }
- }
- const p1 = new Person("lolo");
- p1.say("Learn TypeScript"); // Ok
- // Property 'getClassName' is protected and only accessible within class 'Person' and its subclasses.
- p1.getClassName() // Error
由以上錯誤信息可知,使用 protected 修飾符修飾的方法,只能在當前類或它的子類中使用。
- class Developer extends Person {
- constructor(name: string) {
- super(name);
- console.log(`Base Class:${this.getClassName()}`);
- }
- }
- const p2 = new Developer("semlinker"); // 輸出:"Base Class:Person"
5.3 private
private 修飾的屬性或者方法是私有的,只能在類的內部進行訪問。
- class Person {
- constructor(private id: number, public name: string) {}
- }
- const p1 = new Person(28, "lolo");
- // Property 'id' is private and only accessible within class 'Person'.(2341)
- p1.id // Error
- p1.name // OK
由以上錯誤信息可知,使用 private 修飾符修飾的屬性,只能在當前類內部訪問。但真的是這樣么?其實這只是 TS 類型檢查器給我們的提示,在運行時我們還是可以訪問 Person 實例的 id 屬性。不相信的話,我們來看一下編譯生成的 ES5 代碼:
- "use strict";
- var Person = /** @class */ (function () {
- function Person(id, name) {
- this.id = id;
- this.name = name;
- }
- return Person;
- }());
- var p1 = new Person(28, "lolo");
5.4 私有字段
針對上面的問題,TypeScript 團隊在 3.8 版本就開始支持 ECMAScript 私有字段,使用方式如下:
- class Person {
- #name: string;
- constructor(name: string) {
- this.#name = name;
- }
- }
- let semlinker = new Person("semlinker");
- // Property '#name' is not accessible outside class 'Person' because it has a private identifier.
- semlinker.#name // Error
那么 ECMAScript 私有字段 跟 private 修飾符相比,有什么特別之處么?這里我們來看一下編譯生成的 ES2015 代碼:
- "use strict";
- var __classPrivateFieldSet = // 省略相關代碼
- var _Person_name;
- class Person {
- constructor(name) {
- _Person_name.set(this, void 0);
- __classPrivateFieldSet(this, _Person_name, name, "f");
- }
- }
- _Person_name = new WeakMap();
- let semlinker = new Person("Semlinker");
觀察以上的結果可知,在處理私有字段時使用到了 ES2015 新增的 WeakMap 數(shù)據(jù)類型,如果你對 WeakMap 還不了解的話,可以閱讀 你不知道的 WeakMap 這篇文章。下面我們來總結一下,私有字段與常規(guī)屬性(甚至使用 private 修飾符聲明的屬性)不同之處:
- 私有字段以 # 字符開頭,有時我們稱之為私有名稱;
- 每個私有字段名稱都唯一地限定于其包含的類;
- 不能在私有字段上使用 TypeScript 可訪問性修飾符(如 public 或 private);
- 私有字段不能在包含的類之外訪問,甚至不能被檢測到。
六、類表達式
TypeScript 1.6 添加了對 ES6 類表達式的支持。類表達式是用來定義類的一種語法。和函數(shù)表達式相同的一點是,類表達式可以是命名也可以是匿名的。如果是命名類表達式,這個名字只能在類體內部才能訪問到。
類表達式的語法如下所示([] 方括號表示是可選的):
- const MyClass = class [className] [extends] {
- // class body
- };
基于類表達式的語法,我們可以定義一個 Point 類:
- let Point = class {
- constructor(public x: number, public y: number) {}
- public length() {
- return Math.sqrt(this.x * this.x + this.y * this.y);
- }
- }
- let p = new Point(3, 4);
- console.log(p.length()); // 輸出:5
需要注意在使用類表達式定義類的時候,我們也可以使用 extends 關鍵字。篇幅有限,這里就不展開介紹了,感興趣的小伙伴可以自行測試一下。
七、泛型類
在類中使用泛型也很簡單,我們只需要在類名后面,使用
- class Person<T> {
- constructor(
- public cid: T,
- public name: string
- ) {}
- }
- let p1 = new Person<number>(28, "Lolo");
- let p2 = new Person<string>("exe", "Semlinker");
接下來我們以實例化 p1 為例,來分析一下其處理過程:
- 在實例化 Person 對象時,我們傳入 number 類型和相應的構造參數(shù);
- 之后在 Person 類中,類型變量 T 的值變成 number 類型;
- 最后構造函數(shù) cid 的參數(shù)類型也會變成 number 類型。
相信看到這里一些讀者會有疑問,我們什么時候需要使用泛型呢?通常在決定是否使用泛型時,我們有以下兩個參考標準:
- 當你的函數(shù)、接口或類將處理多種數(shù)據(jù)類型時;
- 當函數(shù)、接口或類在多個地方使用該數(shù)據(jù)類型時。
八、構造簽名
在 TypeScript 接口中,你可以使用 new 關鍵字來描述一個構造函數(shù):
- interface Point {
- new (x: number, y: number): Point;
- }
以上接口中的 new (x: number, y: number) 我們稱之為構造簽名,其語法如下:
- ConstructSignature:new TypeParametersopt ( ParameterListopt ) TypeAnnotationopt
在上述的構造簽名中,TypeParametersopt 、ParameterListopt 和 TypeAnnotationopt 分別表示:可選的類型參數(shù)、可選的參數(shù)列表和可選的類型注解。那么了解構造簽名有什么用呢?這里我們先來看個例子:
- interface Point {
- new (x: number, y: number): Point;
- x: number;
- y: number;
- }
- class Point2D implements Point {
- readonly x: number;
- readonly y: number;
- constructor(x: number, y: number) {
- this.x = x;
- this.y = y;
- }
- }
- const point: Point = new Point2D(1, 2); // Error
對于以上的代碼,TypeScript 編譯器(v4.4.3)會提示以下錯誤信息:
- Type 'Point2D' is not assignable to type 'Point'.
- Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.
要解決這個問題,我們就需要把對前面定義的 Point 接口進行分離:
- interface Point {
- x: number;
- y: number;
- }
- interface PointConstructor {
- new (x: number, y: number): Point;
- }
完成接口拆分之后,除了前面已經定義的 Point2D 類之外,我們又定義了一個 newPoint 工廠函數(shù),該函數(shù)用于根據(jù)傳入的 PointConstructor 類型的構造函數(shù),來創(chuàng)建對應的 Point 對象。
- class Point2D implements Point {
- readonly x: number;
- readonly y: number;
- constructor(x: number, y: number) {
- this.x = x;
- this.y = y;
- }
- }
- function newPoint( // 工廠方法
- pointConstructor: PointConstructor,
- x: number,
- y: number
- ): Point {
- return new pointConstructor(x, y);
- }
- const point: Point = newPoint(Point2D, 1, 2);
九、抽象構造簽名
在 TypeScript 4.2 版本中引入了抽象構造簽名,用于解決以下的問題:
- type ConstructorFunction = new (...args: any[]) => any;
- abstract class Utilities {}
- // Type 'typeof Utilities' is not assignable to type 'ConstructorFunction'.
- // Cannot assign an abstract constructor type to a non-abstract constructor type.
- let UtilityClass: ConstructorFunction = Utilities; // Error.
由以上的錯誤信息可知,我們不能把抽象構造器類型分配給非抽象的構造器類型。針對這個問題,我們需要使用 abstract 修飾符:
- declare type ConstructorFunction = abstract new (...args: any[]) => any;
需要注意的是,對于抽象構造器類型,我們也可以傳入具體的實現(xiàn)類:
- declare type ConstructorFunction = abstract new (...args: any[]) => any;
- abstract class Utilities {}
- class UtilitiesConcrete extends Utilities {}
- let UtilityClass: ConstructorFunction = Utilities; // Ok
- let UtilityClass1: ConstructorFunction = UtilitiesConcrete; // Ok
而對于 TypeScript 4.2 以下的版本,我們可以通過以下方式來解決上面的問題:
- type Constructor<T> = Function & { prototype: T }
- abstract class Utilities {}
- class UtilitiesConcrete extends Utilities {}
- let UtilityClass: Constructor<Utilities> = Utilities;
- let UtilityClass1: Constructor<UtilitiesConcrete> = UtilitiesConcrete;
介紹完抽象構造簽名,最后我們來簡單介紹一下 class type 與 typeof class type 的區(qū)別。
十、class type 與 typeof class type
- class Person {
- static cid: string = "exe";
- name: string; // 成員屬性
- static printCid() { // 定義靜態(tài)方法
- console.log(Person.cid);
- }
- constructor(name: string) { // 類的構造函數(shù)
- this.name = name;
- }
- say(words: string) :void { // 定義成員方法
- console.log(`${this.name} says:${words}`);
- }
- }
- // Property 'say' is missing in type 'typeof Person' but required in type 'Person'.
- let p1: Person = Person; // Error
- let p2: Person = new Person("Semlinker"); // Ok
- // Type 'Person' is missing the following properties from type 'typeof Person': prototype, cid, printCid
- let p3: typeof Person = new Person("Lolo"); // Error
- let p4: typeof Person = Person; // Ok
通過觀察以上的代碼,我們可以得出以下結論:
- 當使用 Person 類作為類型時,可以約束變量的值必須為 Person 類的實例;
- 當使用 typeof Person 作為類型時,可以約束變量的值必須包含該類上的靜態(tài)屬性和方法。
此外,需要注意的是 TypeScript 使用的是 結構化 類型系統(tǒng),與 Java/C++ 所采用的 名義化 類型系統(tǒng)是不一樣的,所以以下代碼在 TS 中是可以正常運行的:
- class Person {
- constructor(public name: string) {}
- }
- class SuperMan {
- constructor(public name: string) {}
- }
- let p1: SuperMan = new Person("Semlinker"); // Ok
好的,在日常工作中,TypeScript 類比較常見的知識,就介紹到這里。
本文轉載自微信公眾號「全棧修仙之路」