TS快速入門手冊---車門焊死,誰也別想下車(萬字長文 高能預警!!!!!)

TypeScript 初體驗 - 環境搭建與編譯執行

學習目標

  • 學會搭建 TypeScript 環境
  • 掌握 TypeScript 代碼的編譯與運行

環境搭建

TypeScript 編寫的程序並不能直接經過瀏覽器運行,咱們須要先經過 TypeScript 編譯器把 TypeScript 代碼編譯成 JavaScript 代碼javascript

TypeScript 的編譯器是基於 Node.js 的,因此咱們須要先安裝 Node.jscss

安裝 Node.js

nodejs.orghtml

安裝完成之後,能夠經過 終端 或者 cmd 等命令行工具來調用 node前端

# 查看當前 node 版本
node -v
複製代碼

安裝 TypeScript 編譯器

經過 NPM 包管理工具安裝 TypeScript 編譯器java

npm i -g typescript
複製代碼

安裝完成之後,咱們能夠經過命令 tsc 來調用編譯器node

# 查看當前 tsc 編譯器版本
tsc -v
複製代碼

編寫代碼

代碼編輯器 - vscodees6

vsCodeTypeScript都是微軟的產品,vsCode 自己就是基於 TypeScript 進行開發的,vsCodeTypeScript 有着自然友好的支持web

code.visualstudio.com/ajax

TypeScript 文件算法

默認狀況下,TypeScript 的文件的後綴爲 .ts

TypeScript 代碼

// ./src/hello.ts
let str: string = 'Typescript';
複製代碼

編譯執行

使用咱們安裝的 TypeScript 編譯器 tsc.ts 文件進行編譯

tsc ./src/hello.ts
複製代碼

默認狀況下會在當前文件所在目錄下生成同名的 js 文件

一些有用的編譯選項

編譯命令 tsc 還支持許多編譯選項,這裏我先來了解幾個比較經常使用的

--outDir

指定編譯文件輸出目錄

tsc --outDir ./dist ./src/hello.ts
複製代碼

--target

指定編譯的代碼版本目標,默認爲 ES3

tsc --outDir ./dist --target ES6 ./src/hello.ts
複製代碼

--watch

在監聽模式下運行,當文件發生改變的時候自動編譯

tsc --outDir ./dist --target ES6 --watch ./src/hello.ts
複製代碼

經過上面幾個例子,咱們基本能夠了解 tsc 的使用了,可是你們應該也發現了,若是每次編譯都輸入這麼一大堆的選項實際上是很繁瑣的,好在TypeScript 編譯爲咱們提供了一個更增強大且方便的方式,編譯配置文件:tsconfig.json,咱們能夠把上面的編譯選項保存到這個配置文件中

編譯配置文件

咱們能夠把編譯的一些選項保存在一個指定的 json 文件中,默認狀況下 tsc 命令運行的時候會自動去加載運行命令所在的目錄下的 tsconfig.json 文件,配置文件格式以下

{
	"compilerOptions": {
		"outDir": "./dist",
		"target": "ES2015",
    "watch": true,
	},
  // ** : 全部目錄(包括子目錄)
  // * : 全部文件,也能夠指定類型 *.ts
  "include": ["./src/**/*"]
}
複製代碼

有了單獨的配置文件,咱們就能夠直接運行

tsc
複製代碼

指定加載的配置文件

使用 --project-p 指定配置文件目錄,會默認加載該目錄下的 tsconfig.json 文件

tsc -p ./configs
複製代碼

也能夠指定某個具體的配置文件

tsc -p ./configs/ts.json
複製代碼

類型系統初識

學習目標

  • 瞭解類型系統
    • 類型標註
    • 類型檢測的好處
    • 使用場景
  • 掌握經常使用的類型標註的使用

什麼是類型

程序 = 數據結構 + 算法 = 各類格式的數據 + 處理數據的邏輯

數據是有格式(類型)的

  • 數字、布爾值、字符
  • 數組、集合

程序是可能有錯誤的

  • 計算錯誤(對非數字類型數據進行一些數學運算)
  • 調用一個不存在的方法

不一樣類型的數據有不一樣的操做方式或方法,如:字符串類型的數據就不該該直接參與數學運算

動態類型語言 & 靜態類型語言

動態類型語言

程序運行期間才作數據類型檢查的語言,如:JavaScript

靜態類型語言

程序編譯期間作數據類型檢查的語言,如:Java

靜態類型語言的優缺點

優勢

  • 程序編譯階段(配合IDE、編輯器甚至能夠在編碼階段)便可發現一些潛在錯誤,避免程序在生產環境運行了之後再出現錯誤
  • 編碼規範、有利於團隊開發協做、也更有利於大型項目開發、項目重構
  • 配合IDE、編輯器提供更強大的代碼智能提示/檢查
  • 代碼即文檔

缺點

  • 麻煩
  • 缺乏靈活性

動態類型語言

優勢

  • 靜態類型語言的缺點

缺點

  • 靜態類型語言的優勢

靜態類型語言的核心 :  類型系統

什麼是類型系統

類型系統包含兩個重要組成部分

  • 類型標註(定義、註解) - typing
  • 類型檢測(檢查) - type-checking

類型標註

類型標註就是在代碼中給數據(變量、函數(參數、返回值))添加類型說明,當一個變量或者函數(參數)等被標註之後就不能存儲或傳入與標註類型不符合的類型

有了標註,TypeScript 編譯器就能按照標註對這些數據進行類型合法檢測。

有了標註,各類編輯器、IDE等就能進行智能提示

類型檢測

顧名思義,就是對數據的類型進行檢測。注意這裏,重點是類型兩字。

類型系統檢測的是類型,不是具體值(雖然,某些時候也能夠檢測值),好比某個參數的取值範圍(1-100之間),咱們不能依靠類型系統來完成這個檢測,它應該是咱們的業務層具體邏輯,類型系統檢測的是它的值類型是否爲數字!

類型標註

TypeScript 中,類型標註的基本語法格式爲:

數據載體:類型
複製代碼

TypeScript 的類型標註,咱們能夠分爲

  • 基礎的簡單的類型標註
  • 高級的深刻的類型標註

基礎的簡單的類型標註

  • 基礎類型
  • 空和未定義類型
  • 對象類型
  • 數組類型
  • 元組類型
  • 枚舉類型
  • 無值類型
  • Never類型
  • 任意類型
  • 未知類型(Version3.0 Added)

基礎類型

基礎類型包含:stringnumberboolean

標註語法

let title: string = '吧';
let n: number = 100;
let isOk: boolean = true;
複製代碼

空和未定義類型

由於在 NullUndefined 這兩種類型有且只有一個值,在標註一個變量爲 NullUndefined 類型,那就表示該變量不能修改了

let a: null;
// ok
a = null;
// error
a = 1;
複製代碼

默認狀況下 nullundefined 是全部類型的子類型。 就是說你能夠把 nullundefined 其它類型的變量

let a: number;
// ok
a = null;
複製代碼

若是一個變量聲明瞭,可是未賦值,那麼該變量的值爲 undefined,可是若是它同時也沒有標註類型的話,默認類型爲 anyany 類型後面有詳細說明

// 類型爲 `number`,值爲 `undefined`
let a: number;
// 類型爲 `any`,值爲 `undefined`
複製代碼

小技巧

由於 nullundefined 都是其它類型的子類型,因此默認狀況下會有一些隱藏的問題

let a:number;
a = null;
// ok(實際運行是有問題的)
a.toFixed(1);
複製代碼

小技巧:指定 strictNullChecks 配置爲 true,能夠有效的檢測 null 或者 undefined,避免不少常見問題

let a:number;
a = null;
// error
a.toFixed(1);
複製代碼

也可使咱們程序編寫更加嚴謹

let ele = document.querySelector('div');
// 獲取元素的方法返回的類型可能會包含 null,因此最好是先進行必要的判斷,再進行操做
if (ele) {
		ele.style.display = 'none';
}
複製代碼

對象類型

內置對象類型

JavaScript 中,有許多的內置對象,好比:Object、Array、Date……,咱們能夠經過對象的 構造函數 或者 來進行標註

let a: object = {};
// 數組這裏標註格式有點不太同樣,後面咱們在數組標註中進行詳細講解
let arr: Array<number> = [1,2,3];
let d1: Date = new Date();

複製代碼

自定義對象類型

另一種狀況,許多時候,咱們可能須要自定義結構的對象。這個時候,咱們能夠:

  • 字面量標註
  • 接口
  • 定義 或者 構造函數

字面量標註:

let a: {username: string; age: number} = {
  username: 'zMouse',
  age: 35
};
// ok
a.username;
a.age;
// error
a.gender;

複製代碼

優勢 : 方便、直接

缺點 : 不利於複用和維護

接口:

// 這裏使用了 interface 關鍵字,在後面的接口章節中會詳細講解
interface Person {
  username: string;
  age: number;
};
let a: Person = {
  username: 'zMouse',
  age: 35
};
// ok
a.username;
a.age;
// error
a.gender;

複製代碼

優勢 : 複用性高

缺點 : 接口只能做爲類型標註使用,不能做爲具體值,它只是一種抽象的結構定義,並非實體,沒有具體功能實現

類與構造函數:

// 類的具體使用,也會在後面的章節中講解
class Person {
	constructor(public username: string, public age: number) {
  }
}
// ok
a.username;
a.age;
// error
a.gender;

複製代碼

優勢 : 功能相對強大,定義實體的同時也定義了對應的類型

缺點 : 複雜,好比只想約束某個函數接收的參數結構,沒有必要去定一個類,使用接口會更加簡單

interface AjaxOptions {
    url: string;
    method: string;
}

function ajax(options: AjaxOptions) {}

ajax({
    url: '',
    method: 'get'
});

複製代碼

擴展

包裝對象:

這裏說的包裝對象其實就是 JavaScript 中的 StringNumberBoolean,咱們知道 string 類型 和 String 類型並不同,在 TypeScript 中也是同樣

let a: string;
a = '1';
// error String有的,string不必定有(對象有的,基礎類型不必定有)
a = new String('1');

let b: String;
b = new String('2');
// ok 和上面正好相反
b = '2';

複製代碼

數組類型

TypeScript 中數組存儲的類型必須一致,因此在標註數組類型的時候,同時要標註數組中存儲的數據類型

使用泛型標註

// <number> 表示數組中存儲的數據類型,泛型具體概念後續會講
let arr1: Array<number> = [];
// ok
arr1.push(100);
// error
arr1.push('吧');

複製代碼

簡單標註

let arr2: string[] = [];
// ok
arr2.push('吧');
// error
arr2.push(1);

複製代碼

元組類型

元組相似數組,可是存儲的元素類型沒必要相同,可是須要注意:

  • 初始化數據的個數以及對應位置標註類型必須一致
  • 越界數據必須是元組標註中的類型之一(標註越界數據能夠不用對應順序 - 聯合類型
let data1: [string, number] = ['吧', 100];
// ok
data1.push(100);
// ok
data1.push('100');
// error
data1.push(true);

複製代碼

枚舉類型

枚舉的做用組織收集一組關聯數據的方式,經過枚舉咱們能夠給一組有關聯意義的數據賦予一些友好的名字

enum HTTP_CODE {
  OK = 200,
  NOT_FOUND = 404,
  METHOD_NOT_ALLOWED
};
// 200
HTTP_CODE.OK;
// 405
HTTP_CODE.METHOD_NOT_ALLOWED;
// error
HTTP_CODE.OK = 1;

複製代碼

注意事項:

  • key 不能是數字
  • value 能夠是數字,稱爲 數字類型枚舉,也能夠是字符串,稱爲 字符串類型枚舉,但不能是其它值,默認爲數字:0
  • 枚舉值能夠省略,若是省略,則:
    • 第一個枚舉值默認爲:0
    • 非第一個枚舉值爲上一個數字枚舉值 + 1
  • 枚舉值爲只讀(常量),初始化後不可修改

字符串類型枚舉

枚舉類型的值,也能夠是字符串類型

enum URLS  {
  USER_REGISETER = '/user/register',
  USER_LOGIN = '/user/login',
  // 若是前一個枚舉值類型爲字符串,則後續枚舉項必須手動賦值
  INDEX = 0
}

複製代碼

注意:若是前一個枚舉值類型爲字符串,則後續枚舉項必須手動賦值

小技巧:枚舉名稱能夠是大寫,也能夠是小寫,推薦使用全大寫(一般使用全大寫的命名方式來標註值爲常量)

無值類型

表示沒有任何數據的類型,一般用於標註無返回值函數的返回值類型,函數默認標註類型爲:void

function fn():void {
  	// 沒有 return 或者 return undefined
}

複製代碼

strictNullChecksfalse 的狀況下,undefinednull 均可以賦值給 void ,可是當 strictNullCheckstrue 的狀況下,只有 undefined 才能夠賦值給 void

Never類型

當一個函數永遠不可能執行 return 的時候,返回的就是 never ,與 void 不一樣,void 是執行了 return, 只是沒有值,never 是不會執行 return,好比拋出錯誤,致使函數終止執行

function fn(): never {
  	throw new Error('error');
}

複製代碼

任意類型

有的時候,咱們並不肯定這個值究竟是什麼類型或者不須要對該值進行類型檢測,就能夠標註爲 any 類型

let a: any;

複製代碼
  • 一個變量申明未賦值且未標註類型的狀況下,默認爲 any 類型
  • 任何類型值均可以賦值給 any 類型
  • any 類型也能夠賦值給任意類型
  • any 類型有任意屬性和方法

注意:標註爲 any 類型,也意味着放棄對該值的類型檢測,同時放棄 IDE 的智能提示

小技巧:當指定 noImplicitAny 配置爲 true,當函數參數出現隱含的 any 類型時報錯

未知類型

unknow,3.0 版本中新增,屬於安全版的 any,可是與 any 不一樣的是:

  • unknow 僅能賦值給 unknowany
  • unknow 沒有任何屬性和方法

函數類型

JavaScript 函數是很是重要的,在 TypeScript 也是如此。一樣的,函數也有本身的類型標註格式

  • 參數
  • 返回值
函數名稱( 參數1: 類型, 參數2: 類型... ): 返回值類型;

複製代碼
function add(x: number, y: number): number {
  	return x + y;
}

複製代碼

函數更多的細節內容,在後期有專門的章節來進行深刻的探討

高級類型

學習目標

  • 使用 聯合類型交叉類型字面量類型 來知足更多的標註需求
  • 使用 類型別名類型推導 簡化標註操做
  • 掌握 類型斷言 的使用

聯合類型

聯合類型也能夠稱爲多選類型,當咱們但願標註一個變量爲多個類型之一時能夠選擇聯合類型標註, 的關係

function css(ele: Element, attr: string, value: string|number) {
    // ...
}

let box = document.querySelector('.box');
// document.querySelector 方法返回值就是一個聯合類型
if (box) {
    // ts 會提示有 null 的可能性,加上判斷更嚴謹
    css(box, 'width', '100px');
    css(box, 'opacity', 1);
    css(box, 'opacity', [1,2]);  // 錯誤
}
複製代碼

交叉類型

交叉類型也能夠稱爲合併類型,能夠把多種類型合併到一塊兒成爲一種新的類型,而且 的關係

對一個對象進行擴展:

interface o1 {x: number, y: string};
interface o2 {z: number};

let o: o1 & o2 = Object.assign({}, {x:1,y:'2'}, {z: 100});
複製代碼

小技巧

TypeScript 在編譯過程當中只會轉換語法(好比擴展運算符,箭頭函數等語法進行轉換,對於 API 是不會進行轉換的(也不必轉換,而是引入一些擴展庫進行處理的),若是咱們的代碼中使用了 target 中沒有的 API ,則須要手動進行引入,默認狀況下 TypeScript 會根據 target 載入核心的類型庫

targetes5 時: ["dom", "es5", "scripthost"]

targetes6 時: ["dom", "es6", "dom.iterable", "scripthost"]

若是代碼中使用了這些默認載入庫之外的代碼,則能夠經過 lib 選項來進行設置

www.typescriptlang.org/docs/handbo…

字面量類型

有的時候,咱們但願標註的不是某個類型,而是一個固定值,就可使用字面量類型,配合聯合類型會更有用

function setPosition(ele: Element, direction: 'left' | 'top' | 'right' | 'bottom') {
  	// ...
}

// ok
box && setDirection(box, 'bottom');
// error
box && setDirection(box, 'hehe');
複製代碼

類型別名

有的時候類型標註比較複雜,這個時候咱們能夠類型標註起一個相對簡單的名字

type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
  	// ...
}
複製代碼

使用類型別名定義函數類型

這裏須要注意一下,若是使用 type 來定義函數類型,和接口有點不太相同

type callback = (a: string) => string;
let fn: callback = function(a) {};

// 或者直接
let fn: (a: string) => string = function(a) {}
複製代碼

interface 與 type 的區別

interface

  • 只能描述 object/class/function 的類型
  • 同名 interface 自動合併,利於擴展

type

  • 不能重名
  • 能描述全部數據

類型推導

每次都顯式標註類型會比較麻煩,TypeScript 提供了一種更加方便的特性:類型推導。TypeScript 編譯器會根據當前上下文自動的推導出對應的類型標註,這個過程發生在:

  • 初始化變量
  • 設置函數默認參數值
  • 返回函數值
// 自動推斷 x 爲 number
let x = 1;
// 不能將類型「"a"」分配給類型「number」
x = 'a';

// 函數參數類型、函數返回值會根據對應的默認值和返回值進行自動推斷
function fn(a = 1) {return a * a}
複製代碼

類型斷言

有的時候,咱們可能標註一個更加精確的類型(縮小類型標註範圍),好比:

let img = document.querySelector('#img');
複製代碼

咱們能夠看到 img 的類型爲 Element,而 Element 類型其實只是元素類型的通用類型,若是咱們去訪問 src 這個屬性是有問題的,咱們須要把它的類型標註得更爲精確:HTMLImageElement 類型,這個時候,咱們就可使用類型斷言,它相似於一種 類型轉換:

let img = <HTMLImageElement>document.querySelector('#img');
複製代碼

或者

let img = document.querySelector('#img') as HTMLImageElement;

複製代碼

注意:斷言只是一種預判,並不會數據自己產生實際的做用,即:相似轉換,但並不是真的轉換了

接口

學習目標

  • 理解接口的概念
  • 學會經過接口標註複雜結構的對象

接口定義

前面咱們說到,TypeScript 的核心之一就是對值(數據)所具備的結構進行類型檢查,除了一些前面說到基本類型標註,針對對象類型的數據,除了前面提到的一些方式意外,咱們還能夠經過: Interface (接口),來進行標註。

接口:對複雜的對象類型進行標註的一種方式,或者給其它代碼定義一種契約(好比:類)

接口的基礎語法定義結構特別簡單

interface Point {
    x: number;
    y: number;
}
複製代碼

上面的代碼定義了一個類型,該類型包含兩個屬性,一個 number 類型的 x 和一個 number 類型的 y,接口中多個屬性之間可使用 逗號 或者 分號 進行分隔

咱們能夠經過這個接口來給一個數據進行類型標註

let p1: Point = {
    x: 100,
    y: 100
};
複製代碼

注意:接口是一種 類型 ,不能做爲 使用

interface Point {
    x: number;
    y: number;
}

let p1 = Point;	//錯誤
複製代碼

固然,接口的定義規則遠遠不止這些

可選屬性

接口也能夠定義可選的屬性,經過 ? 來進行標註

interface Point {
    x: number;
    y: number;
    color?: string;
}
複製代碼

其中的 color? 表示該屬性是可選的

只讀屬性

咱們還能夠經過 readonly 來標註屬性爲只讀

interface Point {
    readonly x: number;
    readonly y: number;
}
複製代碼

當咱們標註了一個屬性爲只讀,那麼該屬性除了初始化之外,是不能被再次賦值的

任意屬性

有的時候,咱們但願給一個接口添加任意屬性,能夠經過索引類型來實現

數字類型索引

interface Point {
    x: number;
    y: number;
    [prop: number]: number;
}
複製代碼

字符串類型索引

interface Point {
    x: number;
    y: number;
    [prop: string]: number;
}
複製代碼

數字索引是字符串索引的子類型

注意:索引簽名參數類型必須爲 stringnumber 之一,但二者可同時出現

interface Point {
    [prop1: string]: string;
    [prop2: number]: string;
}
複製代碼

注意:當同時存在數字類型索引和字符串類型索引的時候,數字類型的值類型必須是字符串類型的值類型或子類型

interface Point1 {
    [prop1: string]: string;
    [prop2: number]: number;	// 錯誤
}
interface Point2 {
    [prop1: string]: Object;
    [prop2: number]: Date;	// 正確
}

複製代碼

使用接口描述函數

咱們還可使用接口來描述一個函數

interface IFunc {
  (a: string): string;
}

let fn: IFunc = function(a) {}

複製代碼

注意,若是使用接口來單獨描述一個函數,是沒 key

接口合併

多個同名的接口合併成一個接口

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

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10}

複製代碼
  • 若是合併的接口存在同名的非函數成員,則必須保證他們類型一致,不然編譯報錯
  • 接口中的同名函數則是採用重載(具體後期函數詳解中講解)

函數詳解

學習目標

  • 掌握 TypeScript 中的函數類型標註
  • 函數可選參數和參數默認值
  • 剩餘參數
  • 函數中的 this
  • 函數重載

函數的標註

一個函數的標註包含

  • 參數
  • 返回值
function fn(a: string): string {};
let fn: (a: string) => string = function(a) {};

type callback = (a: string): string;
interface ICallBack {
  (a: string): string;
}

let fn: callback = function(a) {};
let fn: ICallBack = function(a) {};
複製代碼

可選參數和默認參數

可選參數

經過參數名後面添加 ? 來標註該參數是可選的

let div = document.querySelector('div');
function css(el: HTMLElement, attr: string, val?: any) {

}
// 設置
div && css( div, 'width', '100px' );
// 獲取
div && css( div, 'width' );
複製代碼

默認參數

咱們還能夠給參數設置默認值

  • 有默認值的參數也是可選的
  • 設置了默認值的參數能夠根據值自動推導類型
function sort(items: Array<number>, order = 'desc') {}
sort([1,2,3]);

// 也能夠經過聯合類型來限制取值
function sort(items: Array<number>, order:'desc'|'asc' = 'desc') {}
// ok
sort([1,2,3]);
// ok
sort([1,2,3], 'asc');
// error
sort([1,2,3], 'abc');
複製代碼

剩餘參數

剩餘參數是一個數組,因此標註的時候必定要注意

interface IObj {
    [key:string]: any;
}
function merge(target: IObj, ...others: Array<IObj>) {
    return others.reduce( (prev, currnet) => {
        prev = Object.assign(prev, currnet);
        return prev;
    }, target );
}
let newObj = merge({x: 1}, {y: 2}, {z: 3});
複製代碼

函數中的 this

不管是 JavaScript 仍是 TypeScript ,函數中的 this 都是咱們須要關心的,那函數中 this 的類型該如何進行標註呢?

  • 普通函數
  • 箭頭函數

普通函數

對於普通函數而言,this 是會隨着調用環境的變化而變化的,因此默認狀況下,普通函數中的 this 被標註爲 any,但咱們能夠在函數的第一個參數位(它不佔據實際參數位置)上顯式的標註 this 的類型

interface T {
    a: number;
    fn: (x: number) => void;
}

let obj1:T = {
    a: 1,
    fn(x: number) {
        //any類型
        console.log(this);
    }
}


let obj2:T = {
    a: 1,
    fn(this: T, x: number) {
        //經過第一個參數位標註 this 的類型,它對實際參數不會有影響
        console.log(this);
    }
}
obj2.fn(1);
複製代碼

箭頭函數

箭頭函數的 this 不能像普通函數那樣進行標註,它的 this 標註類型取決於它所在的做用域 this 的標註類型

interface T {
    a: number;
    fn: (x: number) => void;
}

let obj2: T = {
    a: 2,
    fn(this: T) {
        return () => {
            // T
            console.log(this);
        }
    }
}
複製代碼

函數重載

有的時候,同一個函數會接收不一樣類型的參數返回不一樣類型的返回值,咱們可使用函數重載來實現,經過下面的例子來體會一下函數重載

function showOrHide(ele: HTMLElement, attr: string, value: 'block'|'none'|number) {
	//
}

let div = document.querySelector('div');

if (div) {
  showOrHide( div, 'display', 'none' );
  showOrHide( div, 'opacity', 1 );
	// error,這裏是有問題的,雖然經過聯合類型可以處理同時接收不一樣類型的參數,可是多個參數之間是一種組合的模式,咱們須要的應該是一種對應的關係
  showOrHide( div, 'display', 1 );
}
複製代碼

咱們來看一下函數重載

function showOrHide(ele: HTMLElement, attr: 'display', value: 'block'|'none');
function showOrHide(ele: HTMLElement, attr: 'opacity', value: number);
function showOrHide(ele: HTMLElement, attr: string, value: any) {
  ele.style[attr] = value;
}

let div = document.querySelector('div');

if (div) {
  showOrHide( div, 'display', 'none' );
  showOrHide( div, 'opacity', 1 );
  // 經過函數重載能夠設置不一樣的參數對應關係
  showOrHide( div, 'display', 1 );
}
複製代碼
  • 重載函數類型只須要定義結構,不須要實體,相似接口
interface PlainObject {
    [key: string]: string|number;
}

function css(ele: HTMLElement, attr: PlainObject);
function css(ele: HTMLElement, attr: string, value: string|number);
function css(ele: HTMLElement, attr: any, value?: any) {
    if (typeof attr === 'string' && value) {
        ele.style[attr] = value;
    }
    if (typeof attr === 'object') {
        for (let key in attr) {
            ele.style[attr] = attr[key];
        }
    }
}

let div = document.querySelector('div');
if (div) {
    css(div, 'width', '100px');
    css(div, {
        width: '100px'
    });

    // error,若是不使用重載,這裏就會有問題了
    css(div, 'width');
}

複製代碼

面向對象編程

學習目標

  • 掌握面向對象編程中類的基本定義與語法
  • 學會使用類修飾符與寄存器
  • 理解並掌握類的實例成員與類的靜態成員的區別與使用
  • 理解類與接口的關係,並熟練使用它們
  • 瞭解類(構造函數)類型與對象類型的區別

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

類的基礎

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

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

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

class

經過 class 就能夠描述和組織一個類的結構,語法:

// 一般類的名稱咱們會使用 大坨峯命名 規則,也就是 (單詞)首字母大寫
class User {
  // 類的特徵都定義在 {} 內部
}
複製代碼

構造函數

經過 class 定義了一個類之後,咱們能夠經過 new 關鍵字來調用該類從而獲得該類型的一個具體對象:也就是實例化。

爲何類能夠像函數同樣去調用呢,其實咱們執行的並非這個類,而是類中包含的一個特殊函數:構造函數 - constructor

class User {
	
	constructor() {
    console.log('實例化...')
  }
  
}
let user1 = new User;
複製代碼
  • 默認狀況下,構造函數是一個空函數

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

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

  • 若是在實例化(new)一個類的時候無需傳入參數,則能夠省略 ()

  • 構造函數 constructor 不容許有return 和返回值類型標註的(由於要返回實例對象)

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

成員屬性與方法定義

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, 'zMouse');
let user2 = new User(2, 'MT');
複製代碼

this 關鍵字

在類內部,咱們能夠經過 this 關鍵字來訪問類的成員屬性和方法

class User {
  id: number;
  username: string;
	
	postArticle(title: string, content: string): void {
    // 在類的內部能夠經過 `this` 來訪問成員屬性和方法
    console.log(`${this.username} 發表了一篇文章: ${title}`)
  }
}
複製代碼

構造函數參數屬性

由於在構造函數中對類成員屬性進行傳參賦值初始化是一個比較常見的場景,因此 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, 'zMouse');
let user2 = new User(2, 'MT');
複製代碼

繼承

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

class VIP extends User {
  
}
複製代碼

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, 'Leo');
vip1.postArticle('標題', '內容');
vip1.postAttachment('1.png');
複製代碼

方法的重寫與重載

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

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, 'Leo');
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, 'Leo');
vip1.postArticle('標題', '內容');
vip1.postArticle('標題', '內容', '1.png');

複製代碼

修飾符

有的時候,咱們但願對類成員(屬性、方法)進行必定的訪問控制,來保證數據的安全,經過 類修飾符 能夠作到這一點,目前 TypeScript 提供了四種修飾符:

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

public 修飾符

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

  • 自身
  • 子類
  • 類外

protected 修飾符

它的訪問級別爲:

  • 自身
  • 子類

private 修飾符

它的訪問級別爲:

  • 自身

readonly 修飾符

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

  • 自身
  • 子類
  • 類外
class User {
  
  constructor( // 能夠訪問,可是一旦肯定不能修改 readonly id: number, // 能夠訪問,可是不能外部修改 protected username: string, // 外部包括子類不能訪問,也不可修改 private password: string ) {
    // ...
  }
	// ...
}

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

複製代碼

寄存器

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

  • getter
  • setter

getter

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

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, 'zMouse', ['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 方法,不然編碼就不能經過

abstract 關鍵字

若是一個方法沒有具體的實現方法,則能夠經過 abstract 關鍵字進行修飾

abstract class Component<T1, T2> {

    public state: T2;

    constructor(
        public props: T1
    ) {
    }

    public abstract render(): string;
}

複製代碼

使用抽象類有一個好處:

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

使用注意事項:

  • abstract 修飾的方法不能有方法體
  • 若是一個類有抽象方法,那麼該類也必須爲抽象的
  • 若是一個類是抽象的,那麼就不能使用 new 進行實例化(由於抽象類表名該類有未實現的方法,因此不容許實例化)
  • 若是一個子類繼承了一個抽象類,那麼該子類就必須實現抽象類中的全部抽象方法,不然該類還得聲明爲抽象的

類與接口

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

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

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('zMouse', 35, '男');
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);

複製代碼

TypeScript 的模塊系統

TS 模塊系統

雖然早期的時候,TypeScript 有一套本身的模塊系統實現,可是隨着更新,以及 JavaScript 模塊化的日趨成熟,TypeScriptESM 模塊系統的支持也是愈來愈完善

模塊

不管是 JavaScript 仍是 TypeScript 都是以一個文件做爲模塊最小單元

  • 任何一個包含了頂級 import 或者 export 的文件都被當成一個模塊
  • 相反的一個文件不帶有頂級的 import 或者 export ,那麼它的內容就是全局可見的

全局模塊

若是一個文件中沒有頂級 import 或者 export ,那麼它的內容就是全局的,整個項目可見的

// a.ts
let a1 = 100;
let a2 = 200;
複製代碼
// b.ts
// ok, 100
console.log(a1);
// error
let a2 = 300;
複製代碼

不推薦使用全局模塊,由於它會容易形成代碼命名衝突(全局變量污染)

文件模塊

任何一個包含了頂級 import 或者 export 的文件都會當作一個模塊,在 TypeScript 中也稱爲外部模塊。

模塊語法

TypeScriptESM 語法相似

導出模塊內部數據

使用 export 導出模塊內部數據(變量、函數、類、類型別名、接口……)

導入外部模塊數據

使用 import 導入外部模塊數據

模塊編譯

TypeScript 編譯器也可以根據相應的編譯參數,把代碼編譯成指定的模塊系統使用的代碼

module 選項

TypeScript 編譯選項中,module 選項是用來指定生成哪一個模塊系統的代碼,可設置的值有:"none""commonjs""amd""udm""es6"/"es2015/esnext""System"

  • target=="es3" or "es5":默認使用 commonjs
  • 其它狀況,默認 es6

模塊導出默認值的問題

若是一個模塊沒有默認導出

// m1.ts
export let obj = {
  x: 1
}
複製代碼

則在引入該模塊的時候,須要使用下列一些方式來導入

// main.ts
// error: 提示 m1 模塊沒有默認導出
import v from './m1'

// 能夠簡單的使用以下方式
import {obj} from './m1'
console.log(obj.x)
// or
import * as m1 from './m1'
console.log(m1.obj.x)
複製代碼

加載非 TS 文件

有的時候,咱們須要引入一些 js 的模塊,好比導入一些第三方的使用 js 而非 ts 編寫的模塊,默認狀況下 tsc 是不對非 ts 模塊文件進行處理的

咱們能夠經過 allowJs 選項開啓該特性

// m1.js
export default 100;
// main.ts
import m1 from './m1.js'
複製代碼

ESM 模塊中的默認值問題

ESM 中模塊能夠設置默認導出值

export default 'hello';
複製代碼

可是在 CommonJSAMD 中是沒有默認值設置的,它們導出的是一個對象(exports

module.exports.obj = {
    x: 100
}
複製代碼

TypeScript 中導入這種模塊的時候會出現 模塊沒有默認導出的錯誤提示

簡單一些的作法:

import * as m from './m1.js'
複製代碼

經過配置選項解決:

allowSyntheticDefaultImports

設置爲:true,容許從沒有設置默認導出的模塊中默認導入。

雖然經過上面的方式能夠解決編譯過程當中的檢測問題,可是編譯後的具體要運行代碼仍是有問題的

esModuleInterop

設置爲:true,則在編譯的同時生成一個 __importDefault 函數,用來處理具體的 default 默認導出

注意:以上設置只能當 module 不爲 es6+ 的狀況下有效

以模塊的方式加載 JSON 格式的文件

TypeScript 2.9+ 版本添加了一個新的編譯選項:resolveJsonModule,它容許咱們把一個 JSON 文件做爲模塊進行加載

resolveJsonModule

設置爲:true ,能夠把 json 文件做爲一個模塊進行解析

data.json

{
    "name": "zMouse",
    "age": 35,
    "gender": "男"
}
複製代碼

ts文件

import * as userData from './data.json';
console.log(userData.name);
複製代碼

模塊解析策略

什麼是模塊解析

模塊解析是指編譯器在查找導入模塊內容時所遵循的流程。

相對與非相對模塊導入

根據模塊引用是相對的仍是非相對的,模塊導入會以不一樣的方式解析。

相對導入

相對導入是以 /./../ 開頭的引用

// 導入根目錄下的 m1 模塊文件
import m1 from '/m1'
// 導入當前目錄下的 mods 目錄下的 m2 模塊文件
import m2 from './mods/m2'
// 導入上級目錄下的 m3 模塊文件
import m3 from '../m3'
複製代碼

非相對導入

全部其它形式的導入被看成非相對的

import m1 from 'm1'
複製代碼

模塊解析策略

爲了兼容不一樣的模塊系統(CommonJSESM),TypeScript 支持兩種不一樣的模塊解析策略:NodeClassic,當 --module 選項爲:AMDSystemES2015 的時候,默認爲 Classic ,其它狀況爲 Node

--moduleResolution 選項

除了根據 --module 選項自動選擇默認模塊系統類型,咱們還能夠經過 --moduleResolution 選項來手動指定解析策略

// tsconfig.json
{
  ...,
  "moduleResolution": "node"
}
複製代碼

Classic 模塊解析策略

該策略是 TypeScript 之前的默認解析策略,它已經被新的 Node 策略所取代,如今使用該策略主要是爲了向後兼容

相對導入

// /src/m1/a.ts
import b from './b.ts'
複製代碼

解析查找流程:

  1. src/m1/b.ts

默認後綴補全

// /src/m1/a.ts
import b from './b'
複製代碼

解析查找流程:

  1. /src/m1/b.ts

  2. /src/m1/b.d.ts

非相對導入

// /src/m1/a.ts
import b from 'b'
複製代碼

對於非相對模塊的導入,則會從包含導入文件的目錄開始依次向上級目錄遍歷查找,直到根目錄爲止

  1. /src/m1/b.ts

  2. /src/m1/b.d.ts

  3. /src/b.ts

  4. /src/b.d.ts

  5. /b.ts

  6. /b.d.ts

Node 模塊解析策略

該解析策略是參照了 Node.js 的模塊解析機制

相對導入

// node.js
// /src/m1/a.js
import b from './b'
複製代碼

Classic 中,模塊只會按照單個的文件進行查找,可是在 Node.js 中,會首先按照單個文件進行查找,若是不存在,則會按照目錄進行查找

  1. /src/m1/b.js
  2. /src/m1/b/package.json中'main'中指定的文件
  3. /src/m1/b/index.js

非相對導入

// node.js
// /src/m1/a.js
import b from 'b'
複製代碼

對於非相對導入模塊,解析是很特殊的,Node.js 會這一個特殊文件夾 node_modules 裏查找,而且在查找過程當中從當前目錄的 node_modules 目錄下逐級向上級文件夾進行查找

  1. /src/m1/node_modules/b.js
  2. /src/m1/node_modules/b/package.json中'main'中指定的文件
  3. /src/m1/node_modules/b/index.js
  4. /src/node_modules/b.js
  5. /src/node_modules/b/package.json中'main'中指定的文件
  6. /src/node_modules/b/index.js
  7. /node_modules/b.js
  8. /node_modules/b/package.json中'main'中指定的文件
  9. /node_modules/b/index.js

TypeScript 模塊解析策略

TypeScript 如今使用了與 Node.js 相似的模塊解析策略,可是 TypeScript 增長了其它幾個源文件擴展名的查找(.ts.tsx.d.ts),同時 TypeScriptpackage.json 裏使用字段 types 來表示 main 的意義

命名空間

命名空間

TS 中,exportimport 稱爲 外部模塊,TS 中還支持一種內部模塊 namespace,它的主要做用只是單純的在文件內部(模塊內容)隔離做用域

namespace k1 {
    let a = 10;
    export var obj = {
        a
    }
}

namespace k2 {
    let a = 20;
    console.log(k1.obj);
}
複製代碼

裝飾器

學習目標

  • 瞭解裝飾器語法,學會使用裝飾器對類進行擴展
  • 清楚裝飾器執行順序
  • 瞭解元數據以及針對裝飾器的元數據編程

什麼是裝飾器

裝飾器-DecoratorsTypeScript 中是一種能夠在不修改類代碼的基礎上經過添加標註的方式來對類型進行擴展的一種方式

  • 減小代碼量
  • 提升代碼擴展性、可讀性和維護性

TypeScript 中,裝飾器只能在類中使用

裝飾器語法

裝飾器的使用極其的簡單

  • 裝飾器本質就是一個函數
  • 經過特定語法在特定的位置調用裝飾器函數便可對數據(類、方法、甚至參數等)進行擴展

啓用裝飾器特性

  • experimentalDecorators: true
// 裝飾器函數
function log(target: Function, type: string, descriptor: PropertyDescriptor) {
    let value = descriptor.value;

    descriptor.value = function(a: number, b: number) {
        let result = value(a, b);
        console.log('日誌:', {
            type,
            a,
            b,
            result
        })
        return result;
    }
}

// 原始類
class M {
    @log
    static add(a: number, b: number) {
        return a + b;
    }
    @log
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
複製代碼

裝飾器

裝飾器 是一個函數,它能夠經過 @裝飾器函數 這種特殊的語法附加在 方法訪問符屬性參數 上,對它們進行包裝,而後返回一個包裝後的目標對象(方法訪問符屬性參數 ),裝飾器工做在類的構建階段,而不是使用階段

function 裝飾器1() {}
...

@裝飾器1
class MyClass {
  
  @裝飾器2
  a: number;
  
  @裝飾器3
  static property1: number;
  
  @裝飾器4
  get b() { 
    return 1; 
  }
  
  @裝飾器5
  static get c() {
    return 2;
  }
  
  @裝飾器6
  public method1(@裝飾器5 x: number) {
    //
  }
  
  @裝飾器7
  public static method2() {}
}
複製代碼

類裝飾器

目標

  • 應用於類的構造函數

參數

  • 第一個參數(也只有一個參數)
    • 類的構造函數做爲其惟一的參數

方法裝飾器

目標

  • 應用於類的方法上

參數

  • 第一個參數
    • 靜態方法:類的構造函數
    • 實例方法:類的原型對象
  • 第二個參數
    • 方法名稱
  • 第三個參數
    • 方法描述符對象

屬性裝飾器

目標

  • 應用於類的屬性上

參數

  • 第一個參數
    • 靜態方法:類的構造函數
    • 實例方法:類的原型對象
  • 第二個參數
    • 屬性名稱

訪問器裝飾器

目標

  • 應用於類的訪問器(getter、setter)上

參數

  • 第一個參數
    • 靜態方法:類的構造函數
    • 實例方法:類的原型對象
  • 第二個參數
    • 屬性名稱
  • 第三個參數
    • 方法描述符對象

參數裝飾器

目標

  • 應用在參數上

參數

  • 第一個參數
    • 靜態方法:類的構造函數
    • 實例方法:類的原型對象
  • 第二個參數
    • 方法名稱
  • 第三個參數
    • 參數在函數參數列表中的索引

裝飾器執行順序

實例裝飾器

​ 屬性 => 訪問符 => 參數 => 方法

靜態裝飾器

​ 屬性 => 訪問符 => 參數 => 方法

​ 類

裝飾器工廠

若是咱們須要給裝飾器執行過程當中傳入一些參數的時候,就可使用裝飾器工廠來實現

// 裝飾器函數
function log(callback: Function) {
  	return function(target: Function, type: string, descriptor: PropertyDescriptor) {
     	 	let value = descriptor.value;

        descriptor.value = function(a: number, b: number) {
            let result = value(a, b);
            callback({
                type,
                a,
                b,
                result
            });
            return result;
        }
    }
}

// 原始類
class M {
    @log(function(result: any) {
      	console.log('日誌:', result)
    })
    static add(a: number, b: number) {
        return a + b;
    }
    @log(function(result: any) {
      	localStorage.setItem('log', JSON.stringify(result));
    })
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
複製代碼

元數據

裝飾器 函數中 ,咱們能夠拿到 方法訪問符屬性參數 的基本信息,如它們的名稱,描述符 等,可是咱們想獲取更多信息就須要經過另外的方式來進行:元數據

什麼是元數據?

元數據 :用來描述數據的數據,在咱們的程序中,對象 等都是數據,它們描述了某種數據,另外還有一種數據,它能夠用來描述 對象,這些用來描述數據的數據就是 元數據

好比一首歌曲自己就是一組數據,同時還有一組用來描述歌曲的歌手、格式、時長的數據,那麼這組數據就是歌曲數據的元數據

使用 reflect-metadata

www.npmjs.com/package/ref…

首先,須要安裝 reflect-metadata

npm install reflect-metadata
複製代碼

定義元數據

咱們能夠 方法 等數據定義元數據

  • 元數據會被附加到指定的 方法 等數據之上,可是又不會影響 方法 自己的代碼

設置

Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)

  • metadataKey:meta 數據的 key
  • metadataValue:meta 數據的 值
  • target:meta 數據附加的目標
  • propertyKey:對應的 property key

調用方式

  • 經過 Reflect.defineMetadata 方法調用來添加 元數據

  • 經過 @Reflect.metadata 裝飾器來添加 元數據

import "reflect-metadata"

@Reflect.metadata("n", 1)
class A {
    @Reflect.metadata("n", 2)
    public static method1() {
    }
  
  	@Reflect.metadata("n", 4)
  	public method2() {
    }
}

// or
Reflect.defineMetadata('n', 1, A);
Reflect.defineMetadata('n', 2, A, 'method1');

let obj = new A();
Reflect.defineMetadata('n', 3, obj);
Reflect.defineMetadata('n', 4, obj, 'method2');

console.log(Reflect.getMetadata('n', A));
console.log(Reflect.getMetadata('n', A, ));
複製代碼

獲取

Reflect.getMetadata(metadataKey, target, propertyKey)

參數的含義與 defineMetadata 對應

使用元數據的 log 裝飾器

import "reflect-metadata"

function L(type = 'log') {
  	return function(target: any) {
      	Reflect.defineMetadata("type", type, target);
    }
}
// 裝飾器函數
function log(callback: Function) {
  	return function(target: any, name: string, descriptor: PropertyDescriptor) {
     	 	let value = descriptor.value;
      
      	let type = Reflect.getMetadata("type", target);

        descriptor.value = function(a: number, b: number) {
            let result = value(a, b);
          	if (type === 'log') {
              	console.log('日誌:', {
                  name,
                  a,
                  b,
                  result
                })
            }
          	if (type === 'storage') {
                localStorage.setItem('storageLog', JSON.stringify({
                  name,
                  a,
                  b,
                  result
                }));
            }
            return result;
        }
    }
}

// 原始類
@L('log')
class M {
    @log
    static add(a: number, b: number) {
        return a + b;
    }
    @log
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
複製代碼

使用 emitDecoratorMetadata

tsconfig.json 中有一個配置 emitDecoratorMetadata,開啓該特性,typescript 會在編譯以後自動給 方法訪問符屬性參數 添加以下幾個元數據

  • design:type:被裝飾目標的類型
    • 成員屬性:屬性的標註類型
    • 成員方法:Function 類型
  • design:paramtypes
    • 成員方法:方法形參列表的標註類型
    • 類:構造函數形參列表的標註類型
  • design:returntype
    • 成員方法:函數返回值的標註類型
import "reflect-metadata"

function n(target: any) {
}
function f(name: string) {
    return function(target: any, propertyKey: string, descriptor: any) {
      	console.log( 'design type', Reflect.getMetadata('design:type', target, propertyKey) );
        console.log( 'params type', Reflect.getMetadata('design:paramtypes', target, propertyKey) );
        console.log( 'return type', Reflect.getMetadata('design:returntype', target, propertyKey) );
    }
}
function m(target: any, propertyKey: string) {

}

@n
class B {
    @m
    name: string;

    constructor(a: string) {

    }

    @f('')
    method1(a: string, b: string) {
        return 'a'
    }
}
複製代碼

編譯後

__decorate([
    m,
    __metadata("design:type", String)
], B.prototype, "name", void 0);
__decorate([
    f(''),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", void 0)
], B.prototype, "method1", null);
B = __decorate([
    n,
    __metadata("design:paramtypes", [String])
], B);
複製代碼
相關文章
相關標籤/搜索