2019-10-29:
學習內容:類、類與接口、泛型
補充:
1、ts中interface與class的區別:
interface:接口只聲明成員方法,不做實現。
class:類聲明並實現方法。
也就是說:interface只是定義了這個接口會有什么,但是沒有告訴你具體是什么。
2.extends 與 implement的區別:
(1)extends是繼承父類,只要那個類不是聲明為final或者那個類定義為abstract的就能繼承。
(2)java中不支持多重繼承,但是可以用接口來實現,這樣就要用到implements,繼承只能繼承一個類,但implements可以實現多個接口,用逗號分開就行了
class A extends B implements C,D,E
implements就是實現的意思, 顧名思義它實現一個已經定義好的接口中的方法!
一、類(ES6,ES7,TS類的區別):
傳統方法中,JavaScript 通過構造函數實現類的概念,通過原型鏈實現繼承。而在 ES6 中,我們終於迎來了 class
。
TypeScript 除了實現了所有 ES6 中的類的功能以外,還添加了一些新的用法。
(1)類的概念: What is class?
<-- 知識回顧:ES6中類的用法 -->
屬性和方法:
使用 class
定義類,使用 constructor
定義構造函數。
通過 new
生成新實例的時候,會自動調用構造函數。
class Animal { constructor(name) { this.name = name; } sayHi() { return `My name is ${this.name}`; } // 方法 } let a = new Animal('Jack'); console.log(a.sayHi()); // My name is Jack
類的繼承
使用 extends
關鍵字實現繼承,子類中使用 super
關鍵字來調用父類的構造函數和方法。
class Cat extends Animal { constructor(name) { super(name); // 調用父類的 constructor(name) console.log(this.name); } sayHi() { return 'Meow, ' + super.sayHi(); // 調用父類的 sayHi() } } let c = new Cat('Tom'); // Tom console.log(c.sayHi()); // Meow, My name is Tom
靜態方法
使用 static
修飾符修飾的方法稱為靜態方法,它們不需要實例化,而是直接通過類來調用:
class Animal { static isAnimal(a) { return a instanceof Animal; } } let a = new Animal('Jack'); Animal.isAnimal(a); // true a.isAnimal(a); // TypeError: a.isAnimal is not a function
<-- ES7中類的用法 -->
ES7 中有一些關於類的提案,TypeScript 也實現了它們。
實例屬性:
ES6 中實例的屬性只能通過constroctor中的 this.xxx
來定義,ES7 提案中可以直接在類里面定義:
class Animal { name = 'Jack'; constructor() { // ... } } let a = new Animal(); console.log(a.name); // Jack
靜態屬性:
ES7 提案中,可以使用 static
定義一個靜態屬性:
class Animal { static num = 42; constructor() { // ... } } console.log(Animal.num); // 42
<-- TS中類的用法 -->
(1)公共、私有與受保護的修飾符:
TypeScript 可以使用三種訪問修飾符(Access Modifiers),分別是 public
、private
和 protected
。這些編譯到js是一樣的,也就是說js不設置這三者
-
public
修飾的屬性或方法是公有的,可以在任何地方被訪問到,默認所有的屬性和方法都是public
的 -
private
修飾的屬性或方法是私有的,不能在聲明它的類的外部訪問 -
protected
修飾的屬性或方法是受保護的,它和private
類似,區別是它在子類中也是允許被訪問的
(1)當一個成員被標記成private時,它不能在聲明它的類的外部訪問(在子類和類的實現的對象中都不能訪問。在子類可以通過調用使用這個屬性的方法來間接來使用這個屬性。):
// 實例訪問類成員屬於外部訪問
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; } } new Animal("Cat").name; // 錯誤: 'name' 是私有的.
TypeScript使用的是結構性類型系統。 當我們比較兩種不同的類型時,並不在乎它們從何處而來,如果所有成員的類型都是兼容的,我們就認為它們的類型是兼容的。
然而,當我們比較帶有 private
或 protected
成員的類型的時候,情況就不同了。 如果其中一個類型里包含一個 private
成員,那么只有當另外一個類型中也存在這樣一個 private
成員, 並且它們都是來自同一處聲明時,我們才認為這兩個類型是兼容的。 對於 protected
成員也使用這個規則。
(2)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; } public 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); // 錯誤
我們不能在 Person
類外使用 name
,但是我們仍然可以通過 Employee
類的實例方法訪問,因為 Employee
是由 Person
派生而來的。
當構造函數修飾為 private
時,該類不允許被繼承或者實例化;當構造函數修飾為 protected
時,該類只允許被繼承
例子2: 修飾符還可以使用在構造函數參數中,等同於類中定義該屬性,使代碼更簡潔。
class Animal { // public name: string; public constructor (public name) { this.name = name; } }
(2)readonly
只讀屬性關鍵字,只允許出現在屬性聲明或索引簽名中。
class Animal { readonly name; public constructor(name) { this.name = name; } } let a = new Animal('Jack'); console.log(a.name); // Jack a.name = 'Tom'; // Error // index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.
注意:如果 readonly
和其他訪問修飾符同時存在的話,需要寫在其后面。寫在構造函數中定義該屬性
class Animal { // public readonly name; public constructor(public readonly name) { this.name = name; } }
(3)抽象類:
abstract
用於定義抽象類和其中的抽象方法。
-- 什么是抽象類?
A:首先,抽象類是不允許被實例化的
其次,抽象類中的抽象方法必須被子類實現
abstract class Animal { public name; public constructor(name) { this.name = name; } public abstract sayHi(); } class Cat extends Animal { public sayHi() { console.log(`Meow, My name is ${this.name}`); } } let cat = new Cat('Tom');
需要注意的是,即使是抽象方法,TypeScript 的編譯結果中,仍然會存在這個類。
(4)存取器:
TypeScript支持通過getters/setters來截取對對象成員的訪問。 它能幫助你有效的控制對對象成員的訪問。
例子: 我們把對 fullName
的直接訪問改成了可以檢查密碼的 set
方法。
目標:我們可以修改一下密碼,來驗證一下存取器是否是工作的。當密碼不對時,會提示我們沒有權限去修改員工。
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) { alert(employee.fullName); }
對於存取器有下面幾點需要注意的:
首先,存取器要求你將編譯器設置為輸出ECMAScript 5或更高。 不支持降級到ECMAScript 3。 其次,只帶有 get
不帶有 set
的存取器自動被推斷為 readonly
。 這在從代碼生成 .d.ts
文件時是有幫助的,因為利用這個屬性的用戶會看到不允許夠改變它的值。
使用 getter 和 setter 可以改變屬性的賦值和讀取行為:
class Animal {
constructor(name) {
this.name = name; } get name() { return 'Jack'; } set name(value) { console.log('setter: ' + value); } } let a = new Animal('Kitty'); // setter: Kitty a.name = 'Tom'; // setter: Tom console.log(a.name); // Jack // 賦值時用到set方法,取值時用到get方法
二、類與接口:
接口(Interfaces)可以用於對「對象的形狀(Shape)」進行描述。
對類的一部分行為進行抽象。
(1)類實現(implements)接口:
實現(implements)是面向對象中的一個重要概念。一般來講,一個類只能繼承自另一個類,有時候不同類之間可以有一些共有的特性,這時候就可以把特性提取成接口(interfaces),用 implements
關鍵字來實現。這個特性大大提高了面向對象的靈活性。
舉例來說,門是一個類,防盜門是門的子類。如果防盜門有一個報警器的功能,我們可以簡單的給防盜門添加一個報警方法。這時候如果有另一個類,車也有報警器的功能,就可以考慮把報警器提取出來,作為一個接口,防盜門和車都去實現它
一個類只能繼承自另一個類,但是可以實現多個接口:
interface Alarm { alert(); } interface Light { lightOn(); lightOff(); } class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); } }
(2)接口繼承類:
當接口繼承了一個類類型時,它會繼承類的成員但不包括其實現。 就好像接口聲明了所有類中存在的成員,但並沒有提供具體實現一樣。 接口同樣會繼承到類的private和protected成員。 這意味着當你創建了一個接口繼承了一個擁有私有或受保護的成員的類時,這個接口類型只能被這個類或其子類所實現(implement)。
當你有一個龐大的繼承結構時這很有用,但要指出的是你的代碼只在子類擁有特定屬性時起作用。 這個子類除了繼承至基類外與基類沒有任何關系。
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
一個接口可以繼承多個接口,創建出多個接口的合成接口。
interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;
例子2:
class Control { private state: any; } interface SelectableControl extends Control { select(): void; } // 接口繼承類 class Button extends Control implements SelectableControl { select() { } } class TextBox extends Control { select() { } } // 理解一下為什么它也算SelectableControl 的子類? 接口的定義:除了可用於對類的一部分行為進行抽象以外,也常用於對「對象的形狀(Shape)」進行描述。 // 錯誤:“Image”類型缺少“state”屬性。 class Image implements SelectableControl { select() { } } class Location { }
SelectableControl
包含了Control
的所有成員,包括私有成員state
。 因為 state
是私有成員,所以只能夠是Control
的子類們才能實現SelectableControl
接口。 因為只有 Control
的子類才能夠擁有一個聲明於Control
的私有成員state
,這對私有成員的兼容性是必需的。
在Control
類內部,是允許通過SelectableControl
的實例來訪問私有成員state
的。 實際上, SelectableControl
接口和擁有select
方法的Control
類是一樣的。 Button
和TextBox
類是SelectableControl
的子類(因為它們都繼承自Control
並有select
方法),但Image
和Location
類並不是這樣的。
(3)混合類型:
因為JavaScript其動態靈活的特點,有時你會希望一個對象可以同時具有上面提到的多種類型。
一個例子就是,一個對象可以同時做為函數和對象使用,並帶有額外的屬性。
// c這個對象就是作為函數和對象同時使用 interface Counter { (start: number): string; interval: number; reset(): void; } function getCounter(): Counter { let counter = <Counter>function (start: number) { }; counter.interval = 123; counter.reset = function () { }; return counter; } let c = getCounter(); c(10); c.reset(); c.interval = 5.0;
三、泛型:
泛型(Generics)是指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性。
例子: any和泛型的區別:
使用any:使用any
類型會導致這個函數可以接收任何類型的arg
參數,這樣就丟失了一些信息:傳入的類型與返回的類型應該是相同的。如果我們傳入一個數字,我們只知道任何類型的值都有可能被返回。
function identity(arg: any): any {
return arg; }
給identity添加了類型變量T
。 T
幫助我們捕獲用戶傳入的類型(比如:number
),之后我們就可以使用這個類型。 之后我們再次使用了 T
當做返回值類型。現在我們可以知道參數類型與返回值類型是相同的了。 這允許我們跟蹤函數里使用的類型的信息。
function identity<T>(arg: T): T {
return arg; }
注意我們沒必要使用尖括號(<>
)來明確地傳入類型;編譯器可以查看myString
的值,然后把T
設置為它的類型。 類型推論幫助我們保持代碼精簡和高可讀性。如果編譯器不能夠自動地推斷出類型的話,只能像上面那樣明確的傳入T的類型,在一些復雜的情況下,這是可能出現的。
例子1:
Array<any>
允許數組的每一項都為任意類型,但是沒有准確的定義返回值的類型。我們預期的是,數組中每一項都應該是輸入的 value
的類型。改用泛型:
function createArray<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; } createArray<string>(3, 'x'); // ['x', 'x', 'x']
我們在函數名后添加了 <T>
,其中 T
用來指代任意輸入的類型,在后面的輸入 value: T
和輸出 Array<T>
中即可使用了。
例子2: 一次定義多個泛型參數
function swap<T, U>(tuple: [T, U]): [U, T] { return [tuple[1], tuple[0]]; } swap([7, 'seven']); // ['seven', 7]
例子3: 泛型約束
由於泛型事先不知道類型,當我想這個函數里使用length方法時,會報錯。這時需要對泛型進行約束,滿足可以使用length方法:
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; }
使用了 extends
約束了泛型 T
必須符合接口 Lengthwise
的形狀,也就是必須包含 length
屬性。
例子4: 多個類型參數之間也可以互相約束
extends:繼承
function copyFields<T extends U, U>(target: T, source: U): T { for (let id in source) { target[id] = (<T>source)[id]; } return target; } let x = { a: 1, b: 2, c: 3, d: 4 }; copyFields(x, { b: 10, d: 20 });
上例中,我們使用了兩個類型參數,其中要求 T
繼承 U
,這樣就保證了 U
上不會出現 T
中不存在的字段。
例子5: 泛型接口
使用含有泛型的接口來定義函數的形狀:
interface CreateArrayFunc { <T>(length: number, value: T): Array<T>; } let createArray: CreateArrayFunc; createArray = function<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; } createArray(3, 'x'); // ['x', 'x', 'x']
進一步,我們可以把泛型參數提前到接口名上:
interface CreateArrayFunc<T> { (length: number, value: T): Array<T>; } let createArray: CreateArrayFunc<any>; // 此時在使用泛型接口的時候,需要定義泛型的類型 createArray = function<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; } createArray(3, 'x'); // ['x', 'x', 'x']
例子6: 泛型類
泛型類看上去與泛型接口差不多。 泛型類使用( <>
)括起泛型類型,跟在類名后面。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; };
例子7: 泛型參數的默認類型
當使用泛型時沒有在代碼中直接指定類型參數,從實際值參數中也無法推測出時,這個默認類型就會起作用
function createArray<T = string>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; }