TypeScript 是 JavaScript 的一個超集,支持 ECMAScript 6 標准。
TypeScript 由微軟開發的自由和開源的編程語言。
TypeScript 設計目標是開發大型應用,它可以編譯成純 JavaScript,編譯出來的 JavaScript 可以運行在任何瀏覽器上。
以上為網上對 Typescript 的一些解釋,那我們為什么要學 Typescript?
提到前端我們首先會想到 HTML,CSS,JavaScript 三大家族,我們掌握這三個就可以在前端界獲得一席之地,怎么突然又冒出個 Typescript,真心是學不動了,但是眾所周知,Vue 創始人尤雨溪尤大大已經宣布 Vue3.x 代碼庫將使用 Typescript 編寫,並且在知乎上對於"Typescript 不適合在 Vue 開發業務中使用嗎?" 的提問中做出回答,傳送門:https://www.zhihu.com/question/310485097/answer/591869966,Vue 又是現在國內主流的前端框架之一,只能說,如果不學 Typescript,尤大大都救不了你了。
眾所周知,從本質上來說,JavaScript是一種自由松散語言,它的語法規則並不是那么嚴格。正因為如此,我們就更容易犯錯,而且,即使是在運行的時候,我們也不能找到所有的錯誤。鑒於此,TypeScript作為JavaScript的增強版,它的語法更嚴格,我們在編寫代碼的時候就能夠發現大部分錯誤。不僅如此,按照TypeScript官方的說法,TypeScript使得我們能夠以JavaScript的方式實現自己的構思。TypeScript對面向對象的支持也非常完善,它擁有面向對象編程語言的所有特性。
TypeScript最大的目的是讓程序員更具創造性,提高生產力,它將極大增強JavaScript編寫應用的開發和調試環節,讓JavaScript能夠方便用於編寫大型應用和進行多人協作。
不過目前最后運行時還需要將TypeScript編譯為JavaScript。
那接下來我們就來看看如何安裝使用 Typescript。
在安裝 Typescript 之前我們要先安裝 NodeJs,然后運行
npm install -g typescript
以上命令會在全局環境下安裝 tsc
命令,安裝完成之后,我們就可以在任何地方執行 tsc
命令了。
編譯一個 TypeScript 文件很簡單:
tsc demo.ts
運行上面的代碼,我們就可以將 demo.ts 生成一個可以讓瀏覽器解析的 demo.js 的文件。
我們約定使用 TypeScript 編寫的文件以 .ts
為后綴,用 TypeScript 編寫 React 時,以 .tsx
為后綴。
那么我們在編寫代碼的時候不能每次都手動編譯 .ts 文件,我們想要的是實時編譯 .ts 文件,接下來我們就以 webstorm 編輯器來使 .ts 文件進行實時編譯。其他編輯器可自行百度如何實時編譯。
webstorm 版本:
創建一個 demo 項目,然后在項目中創建一個 tsconfig.json 的文件,內容如下:
這里只是簡單的配置,詳細參數配置:https://www.tslang.cn/docs/handbook/tsconfig-json.html
打開Webstorm,為TypeScript文件更改編譯設置,File->Settings->Tool->File Watchers->TypeScript,這里我們需要選擇TypeScript,但是File Watchers下默認是不存在的。需要點擊右側“+”號,選擇,彈出 New Watcher,設置好圈紅線的部分,點擊ok。勾選“TypeScript”,點擊ok。


File->Settings->Languages & Frameworks->TypeScript
根據上面的操作我們就可以實時編譯 .ts 文件了。
目錄結構如下,在 index.html 中銀潤 test.js
test.ts 會實時編譯為 test.js 文件。
test.ts


index.html 開發者工具中的打印日志
根據上面的步驟我們就可以對 ts 文件進行實時編譯了,接下來我們就來看一下 Typescript 的一些基本用法。
從上圖我們可以看出 Typescript 包含了 ES6 和 ES5,那我們就可以在 typescript 中使用 es5 和 es6,同時進行了擴展,語法上跟我們之前講的 Java 語法有很多相似。
Typescript 基礎類型
Typescript 基礎類型有:布爾類型(boolean)、數字類型(number)、字符串類型(string)、數組類型(array)、元組類型(tuple)、枚舉類型(enum)、任意類型(any)、null 和 undefined 、void、never 類型等,接下來我們就一一來看一下這些類型的應用。
1 /** 2 * 在定義完參數進行賦值時, 3 * 必須按照給定的參數類型進行賦值 4 * 否則會出現編譯問題 5 * 但在頁面當中還是會進行編譯 6 * 但是不提倡這么做 7 */ 8 // boolean 9 let flag: boolean = true; 10 console.log(flag); // true 11 // let flag1:boolean = 123; // Type '123' is not assignable to type 'boolean'. 12 13 // number 14 let num: number = 123; 15 console.log(num); // 123 16 17 // string 18 let str: string = "abc"; 19 console.log(str); // abc 20 21 // array 兩種定義方式 22 let arrS: string[] = ["123", "abc"]; // 數組內元素必須為 string 23 let arrA: Array<number> = [123, 456]; // 數組內元素必須為 number 24 console.log(arrS); // ["123", "abc"] 25 console.log(arrA); // [123, 456] 26 27 // tuple 元祖類型,屬於數組的一種,可以為每個元素指定類型 28 let tup: [number, string] = [123, "abc"]; 29 console.log(tup); // [123, "abc"] 30 // let tup1:[number,string] = ["abc",123]; // 報錯 31 32 // enum 枚舉類型 33 /** 34 * 在日常生活或者開發中 35 * 很多都不能或者不容易使用數據表達 36 * 如:顏色,日期,角色,性別等 37 * 例如在開發中我們常用 -1 表示 error,用 0 表示 success 38 * 這個就可以成為枚舉 39 */ 40 enum Flag { 41 error = -1, 42 success = 0 43 } 44 45 let e: Flag = Flag.error; 46 console.log(e); // -1 47 48 /** 49 * 如果枚舉元素不賦值,則默認取值為下標 50 * 如果某個元素取值為數字 n 51 * 后面的元素如果不取值則默認為 n+1,以此類推 52 * 如果某個元素取值為 string 類型 53 * 后面的元素則必須取值,取值類型無要求 54 */ 55 enum Color { 56 red, blue, black = 4, yellow, green = "green", white = 123 57 } 58 59 let red: Color = Color.red; 60 let blue: Color = Color.blue; 61 let black: Color = Color.black; 62 let yellow: Color = Color.yellow; 63 let green: Color = Color.green; 64 let white: Color = Color.white; 65 console.log(red, blue, black, yellow, green, white); // 0 1 4 5 "green" 123 66 67 // undefined 68 let un: undefined; 69 console.log(un); // undefined 70 71 // 我們也可以通過 | 來賦值多種類元素 72 let uns: number | undefined; 73 74 // any 類型,可以為任意類型 75 let an: any = 123; 76 console.log(an); // 13 77 an = "abc"; 78 console.log(an) // abc
在上面的代碼中,我們演示了一下 Typescript 中的一些基本類型的使用,接下來我們再來看一下在函數中數據類型的應用。
Typescript 函數方法
1 // 如果沒有返回值,則在方法名后面加 :void 2 function test(): void { 3 console.log("test") 4 } 5 6 test(); // test 7 8 // 如果有返回值,則在方法名后面加 :返回值的類型 9 function num(): number { 10 return 123; 11 } 12 13 console.log(num()); // 123 14 15 function str(): string { 16 return "abc"; 17 } 18 19 console.log(str()); // "abc" 20 21 // 定義傳參 22 function getData(name: string, age: number): void { 23 console.log(`${name}--${age}`) 24 } 25 26 getData("張三", 18); // 張三--18 27 // getData("張三"); // 報錯 Expected 2 arguments, but got 1. 28 // getData("張三","18"); // 報錯 Argument of type '"18"' is not assignable to parameter of type 'number'. 29 30 /** 31 * 方法可選參數 32 * 在參數后面添加 ? 33 * 表示該參數為可選參數 34 */ 35 function getInfo(name: string, age?: number): string { 36 if (age) { 37 return `${name}--${age}` 38 } else { 39 return `${name}` 40 } 41 } 42 43 console.log(getInfo("張三", 18)); // 張三--18 44 console.log(getInfo("張三")); // 張三 45 46 /** 47 * 方法默認參數 48 * 在參數后面直接賦值 49 * 表示該參數直接當做了被傳入參數 50 */ 51 function getUser(name: string, age: number = 18): string { 52 if (age) { 53 return `${name}--${age}` 54 } else { 55 return `${name}` 56 } 57 } 58 59 console.log(getUser("張三", 18)); // 張三--18 60 console.log(getUser("張三")); // 張三--18 61 62 /** 63 * 剩余參數 64 * 如果在傳參過程中 65 * 前面的參數已經給定 66 * 在調用函數傳參時會先將 67 * 傳入的參數作為指定參數 68 * 剩余參數必須為最后一個參數傳入 69 */ 70 // 正常的傳參 71 function sum1(...arr: number[]): number { 72 let sum: number = 0; 73 for (let i = 0; i < arr.length; i++) { 74 sum += arr[i]; 75 } 76 return sum; 77 } 78 console.log(sum1(1, 2, 3, 4)); // 10 79 80 // a 作為第一個參數,其余的為剩余參數,即 (a,剩余參數) 81 function sum2(a: number, ...arr: number[]): number { 82 let sum: number = 0; 83 for (let i = 0; i < arr.length; i++) { 84 sum += arr[i]; 85 } 86 return sum; 87 } 88 console.log(sum2(1, 2, 3, 4)); // 10
在上面的代碼中,我們實現了在 ts 中如何定義方法和如何進行方法傳參,跟定義基本類型一樣需要對方法進行有效的規定。
接下來我們再來看一下在 ts 中如何實現類和類的繼承。
Typescript 類
在 ES5 中,我們是通過構造方法和原型鏈的方式進行繼承的,在之前的文章中我們也講過如何實現繼承,傳送門:https://www.cnblogs.com/weijiutao/p/12090916.html,Typescript 包含 ES6,所以本章着重講解一下 ES6 中 class 關鍵字的類和繼承。
1 // 在 ts 中定義類 2 class Person { 3 name: string; 4 5 constructor(name: string) { 6 this.name = name; 7 } 8 9 getName(): void { 10 console.log(this.name); 11 } 12 13 setName(name: string): string { 14 return this.name = name; 15 } 16 17 work(): void { 18 console.log("父類在工作") 19 } 20 } 21 22 let p = new Person("張三"); 23 p.getName(); // 張三 24 p.setName("李四"); 25 p.getName(); // 李四 26 p.work(); // 父類在工作 27 28 // 在 ts 中實現繼承 29 /** 30 * 通過 extends 繼承了 Person 的屬性和方法 31 */ 32 class Student extends Person { 33 constructor(name: string) { 34 super(name); 35 } 36 37 // 子類自己的方法 38 run(): void { 39 console.log(this.name + "在運動") 40 } 41 42 // 子類重寫父類的方法 43 work(): void { 44 console.log("子類在工作") 45 } 46 47 } 48 49 let s = new Student("王五"); 50 s.getName(); // 王五 51 s.run(); // 王五在運動 52 s.work(); // 子類在工作
* 類里面的修飾符
* Typescript 里面定義屬性的時候
* 給我們提供了三種修飾符
* public:公有類型,在類里面、子類、類外面都可以訪問
* protected:保護類型,在類里面,子類里面可以訪問,類外面無法訪問
* private:私有類型,在類里面可以訪問,子類,類外面無法訪問
* 屬性不加修飾符,默認為公有屬性
1 /** 2 * 類里面的修飾符 3 * Typescript 里面定義屬性的時候 4 * 給我們提供了三種修飾符 5 * public:公有類型,在類里面、子類、類外面都可以訪問 6 * protected:保護類型,在類里面,子類里面可以訪問,類外面無法訪問 7 * private:私有類型,在類里面可以訪問,子類,類外面無法訪問 8 * 屬性不加修飾符,默認為公有屬性 9 */ 10 11 class Person { 12 name: string; 13 public age: number = 18; 14 protected sex: string = "男"; 15 private city: string = "北京"; 16 17 constructor(name: string) { 18 this.name = name; 19 } 20 21 // 在本類中訪問 public 類型 22 getName(): void { 23 console.log(this.name); 24 } 25 26 // 在本類中訪問 public 類型 27 getAge(): void { 28 console.log(this.age); 29 } 30 31 // 在本類中訪問 protected 類型 32 getSex(): void { 33 console.log(this.sex); 34 } 35 36 // 在本類中訪問 private 類型 37 getCity(): void { 38 console.log(this.city); 39 } 40 41 } 42 43 let p = new Person("張三"); 44 // 外部訪問 public 類型 45 console.log(p.name); // 張三 46 // 外部訪問 public 類型 47 console.log(p.age); // 18 48 // 外部訪問 protected 類型 49 // console.log(p.sex); // 報錯 Property 'sex' is protected and only accessible within class 'Person' and its subclasses. 50 // 外部訪問 private 類型 51 // console.log(p.city); // 報錯 Property 'city' is private and only accessible within class 'Person'. 52 53 class Student extends Person { 54 constructor(name: string) { 55 super(name); 56 } 57 58 getInfo(): void { 59 // 在子類中訪問 public 屬性 60 console.log(this.name); 61 // 在子類中訪問 private 屬性 62 console.log(this.age); 63 // 在子類中訪問 protected 屬性 64 console.log(this.sex); 65 // 在子類中訪問 private 屬性 66 // console.log(this.city) // 報錯 Property 'city' is private and only accessible within class 'Person'. 67 } 68 } 69 70 let s = new Student("王五"); 71 s.getInfo(); // 王五 18 男
在上面的代碼中我們實現了一下 class 中的修飾符,接下來我們再來看一下 class 的靜態屬性、靜態方法
1 /** 2 * 通過 static 關鍵字可以定義靜態屬性和靜態方法 3 * 靜態屬性和靜態方法直接通過 類. 來實現 4 */ 5 6 class Person { 7 name: string; 8 static age: number = 18; 9 10 constructor(name: string) { 11 this.name = name; 12 } 13 14 run(): void { 15 console.log(`${this.name}在運動`) 16 } 17 18 /** 19 * 靜態方法無法通過 this 調用類里面的屬性 20 * 通過 類. 調用 21 */ 22 static work(): void { 23 console.log("父類靜態方法" + Person.age) 24 } 25 } 26 27 let p = new Person("張三"); 28 p.run(); // 張三在運動 29 console.log(p.name); // 張三 30 31 console.log(Person.age); // 18 32 Person.work(); // 父類靜態方法18
在上面的代碼中我們實現了一下 class 中的靜態屬性、靜態方法,接下來我們再來看一下 class 的多態
1 /** 2 * 我們在定義類方法后 3 * 子類繼承該類,並根據實際情況 4 * 來實現自己所需要的類方法 5 */ 6 class Person { 7 name: string; 8 age: number; 9 10 constructor(name: string) { 11 this.name = name; 12 } 13 14 work(): void { 15 console.log(`${this.name}在工作`) 16 } 17 18 19 } 20 21 class Student extends Person { 22 constructor(name: string) { 23 super(name); 24 } 25 26 run(): void { 27 console.log(`${this.name}在學習`) 28 } 29 } 30 let s = new Student("張三"); 31 s.run(); // 張三在學習 32 33 class Teacher extends Person { 34 constructor(name: string) { 35 super(name); 36 } 37 38 run(): void { 39 console.log(`${this.name}在講課`) 40 } 41 } 42 let t = new Teacher("李四"); 43 t.run(); // 李四在講課
在上面的代碼中,我們定義了 Person 類並定義了一個 work 方法,然后 Student 和 Teacher 類分別繼承了 Person,但是根據自己的角色重寫了 work 方法,這就是一種多態。
接下來我們再來看一下 class 中的抽象類
1 /** 2 * 抽象類是提供其他類繼承的基類,不能被實例化 3 * abstract 關鍵字定義抽象類和抽象方法 4 * 子抽象類中的抽象方法不能包含具體實現,必須在實現類中實現 5 * 抽象類和抽象方法只是用來定義標准 6 */ 7 abstract class Person { 8 name: string; 9 10 constructor(name: string) { 11 this.name = name; 12 } 13 14 abstract work(): any; 15 } 16 17 // 無法被實例化 18 // let p = new Person(); // 報錯 error TS2511: Cannot create an instance of an abstract class. 19 20 class Student extends Person { 21 constructor(name: string) { 22 super(name); 23 } 24 25 work(): any { 26 console.log(`${this.name}在學習`) 27 } 28 } 29 30 let s = new Student("張三"); 31 s.work(); // 張三在學習
Typescript 接口
在面向對象的編程中,接口是一種規范的定義,它定義了行為和動作的規范,在程序設計里面,幾口是一種限制和規范的作用。接口定義了某一批類所需要遵循的規范,接口不關心這些類的內部狀態數據,也不關心這些類里面方法的實現細節,它只規定這批類里面必須提供某些方法,提供這些方法的類就可以滿足實際需要,Typescript 中的接口類似於 Java,同時還增加了更靈活的接口類型,包括屬性、函數、數組類等。
在日常生活中,我們會接觸到很多類似接口的問題,比如 USB 接口,我們在電腦上插鼠標,鍵盤,U盤的時候不用去考慮它到底能不能插進去,只要型號對了就肯定能插進去,接口就相當於一個標准,你要想把鼠標插到我的電腦上,在出廠時就必須遵守該電腦定義的接口標准。
接下來我們就來看一下接口:
1、屬性和類接口
1 interface FullName { 2 firstName:string, 3 lastName:string, 4 sayHi: ()=>string 5 } 6 7 let person:FullName = { 8 firstName:"張", 9 lastName:"三", 10 sayHi: ():string =>{return "hell world"} 11 }; 12 13 console.log(person.firstName); // 張 14 console.log(person.lastName); // 三 15 console.log(person.sayHi()); // hello world
在上面的代碼中,我們通過 interface 編寫了一個 FullName 的接口,然后定義了一個 person 的變量來實現這個接口,那么我們就可以使用該接口里面的屬性了。
2、函數類型接口
1 // 對方法傳入的參數以及返回值進行約束 2 interface encrypted { 3 (key: string, value: string): string 4 } 5 6 // key 和 value 必須符合 encrypted 接口的 string 類型約束 7 let getData: encrypted = (key: string, value: string) => { 8 return `${key}--${value}`; 9 }; 10 // 錯誤寫法 11 // let getData:encrypted = (key:number,value:string)=>{ 12 // return `${key}--${value}`; 13 // }; 14 console.log(getData("name", "張三")); // name--張三
3、數組類型接口
1 interface nameArray { 2 [index:number]:string 3 } 4 5 let list:nameArray = ["張三","李四"]; 6 // let list:namelist = ["張三","李四",123]; // 錯誤元素 123 不是 string 類型
4、接口的繼承
1 /** 2 * 通過 implements 來實現接口的繼承 3 * 同時接口內的屬性和方法在子類中必須重新定義 4 * 可以實現多繼承,class 子類 implements 接口1, 接口2{ } 5 */ 6 7 interface Person { 8 name: string, 9 work: () => void 10 } 11 12 class Student implements Person { 13 name: string; 14 15 constructor(name: string) { 16 this.name = name; 17 } 18 19 work(): void { 20 console.log(`${this.name}在學習`) 21 } 22 } 23 24 let s = new Student("張三"); 25 s.work(); // 張三在學習
5、接口的擴展
1 /** 2 * 通過 extends 來實現接口的擴展 3 * 在實現接口的時候,接口的擴展接口也必須重新定義 4 */ 5 6 interface Animals { 7 name: string, 8 } 9 10 interface Person extends Animals { 11 age: number, 12 } 13 14 let person:Person = { 15 name:"張三", 16 age: 18 17 }; 18 console.log(person); // {name: "張三", age: 18}
Typescript 泛型
在軟件工程中,我們不僅要創建一致的定義良好的 API,同時也要考慮可重用性,組件不僅能夠支持當前的數據類型,同時也能支持未來的數據類型,這在創建大型系統時為你提供了十分靈活的功能。
在 C# 和 Java 這樣的語言中,可以使用泛型來創建可重用的組件,一個組件可以支持多種類型的數據,這樣用戶就可以有自己的數據類型來使用組件。
通俗理解,泛型就是解決函數、類、接口 的復用性,以及對不特定數據類型的支持。
1、泛型函數
現在需要寫一個方法實現,我們傳入的參數和返回的參數保持一致,並且參數必須為 string 或者 number,按照正常的思維我們代碼應該是這樣:
1 function getData1(value: string): string { 2 return value; 3 } 4 5 function getData2(value: number): number { 6 return value; 7 } 8 9 // 返回 string 類型 10 getData1("abc"); 11 // 返回 number 類型 12 getData2(123);
在上面的代碼中,我們需要定義兩個不同的方法來分別實現 string 和 number 類型數據的返回,這樣會造成大量重復代碼,當然我們可以將返回類型變為 any 類型來解決,如下:
1 function getData(value: any): any { 2 return value; 3 } 4 5 // 返回 string 類型 6 getData("abc"); 7 // 返回 number 類型 8 getData(123);
使用 any 類型可以解決我們的問題,但是其實是放棄了類型檢驗,和普通 js 就沒有什么區別了。
接下來我們來看一下泛型是如何解決該問題的:
1 /** 2 * 定義泛型類型 T 3 * 我們需要 T 是什么類型 4 * 直接定義 T 的類型即可 5 * @param value 6 */ 7 function getData<T>(value: T): T { 8 return value; 9 } 10 11 // 返回 string 類型 12 getData<string>("abc"); 13 // 返回 number 類型 14 getData<number>(123); 15 16 // 錯誤寫法 17 // getData<string>(456); // 報錯 rgument of type '456' is not assignable to parameter of type 'string'.
在上面的代碼中,我們通過 T 來定義方法的泛型,這樣方法在傳入時需要先定義參數的類型,然后定義返回數據類型,這樣就實現了上面的問題。
2、泛型類
現在有這樣一個問題,我們要定義一個類,該類中 toString 方法返回值為一個二維坐標系上的某個點,按照之前我們講的類的內容,可以寫出如下代碼:
1 class Point { 2 x: number; 3 y: number; 4 5 constructor(x: number, y: number) { 6 this.x = x; 7 this.y = y; 8 } 9 10 toString() { 11 return "(" + this.x + "," + this.y + ")"; 12 } 13 } 14 15 let p = new Point(1, 2); 16 // 坐標系點 (1,2) 17 console.log(p.toString()); // (1,2)
上面的代碼可以實現我們想要的功能,但是現在又有新需求,坐標系上的點可以為 string 類型的數據,入(一,二),而我們在定義傳入 x,y 時候已經定義了它傳入的類型必須是 number 類型,如果向函數泛型那樣定義 any 類型的話其實就失去了類型校驗,跟普通的 js 沒有什么區別了,這時候就需要泛型來解決了,如下:
1 /** 2 * Point 類的泛型 T 3 * Point 需要傳入什么參數類型 4 * 就讓 T 是什么類型即可 5 */ 6 class Point<T> { 7 x: T; 8 y: T; 9 10 constructor(x: T, y: T) { 11 this.x = x; 12 this.y = y; 13 } 14 15 toString() { 16 return "(" + this.x + "," + this.y + ")"; 17 } 18 } 19 20 let p1 = new Point<number>(1, 2); 21 // 坐標系點 (1,2) 22 console.log(p1.toString()); // (1,2) 23 24 let p2 = new Point<string>("一", "二"); 25 // 坐標系點 (一,二) 26 console.log(p1.toString()); // (一,二)
在上面的代碼中,我們通過 T 實現了 Point 類型的泛型,Point 需要傳入什么類型的參數,我們就將 T 定義成什么類型的數據即可。
3、泛型接口
還是上面的問題,不過這次我們是將 Point 寫成接口的形式,如下:
1 interface Point { 2 (x: number, y: number): string 3 } 4 5 let getPoint: Point = (x, y): string => "(" + x + "," + y + ")"; 6 7 console.log(getPoint(1, 2)); // (1,2)
在上面的代碼中,我們將 x 和 y 定義為 number 類型,這樣在傳入參數時就只能是 number 類型了,按照上面泛型函數和泛型類的方法,我們將 number 變為 T,如下:
1 interface Point<T> { 2 (x: T, y: T): string 3 } 4 5 let getPoint1: Point<number> = (x, y): string => "(" + x + "," + y + ")"; 6 // 坐標系點 (1,2) 7 console.log(getPoint1(1, 2)); // (1,2) 8 9 let getPoint2: Point<string> = (x, y): string => "(" + x + "," + y + ")"; 10 // 坐標系點 (一,二) 11 console.log(getPoint2("一", "二")); // (一,二)
通過 T 我們將 Point 接口傳入的參數 x,y 進行泛型,這樣我們就可以根據自己的需求傳入我們想要的參數類型了。
TypeScript 模塊
TypeScript 模塊的設計理念是可以更換的組織代碼。
模塊是在其自身的作用域里執行,並不是在全局作用域,這意味着定義在模塊里面的變量、函數和類等在模塊外部是不可見的,除非明確地使用 export 導出它們。類似地,我們必須通過 import 導入其他模塊導出的變量、函數、類等。
兩個模塊之間的關系是通過在文件級別上使用 import 和 export 建立的。
模塊使用模塊加載器去導入其它的模塊。 在運行時,模塊加載器的作用是在執行此模塊代碼前去查找並執行這個模塊的所有依賴。 大家最熟知的JavaScript模塊加載器是服務於 Node.js 的 CommonJS 和服務於 Web 應用的 Require.js。
此外還有有 SystemJs 和 Webpack。
模塊導出使用關鍵字 export 關鍵字,要在另外一個文件使用該模塊就需要使用 import 關鍵字來導入。
接下來我們就來實現一下:
在上圖中我們通過 module.ts 中 export 關鍵字導出我們封裝好的屬性和方法,在 test.ts 中我們就可以使用 import 關鍵字來導入我們想要的屬性和放大了。
我們也可以通過 export default 來導出模塊,如下
但是 export default 在模塊導出只能引用一次,導入時也不能使用 { },而是直接導入模塊即可。
Typescript 命名空間
在代碼量較大的情況下,為了避免各種命名沖突,可將相似功能的函數,類,接口等放置到命名空間內。同 Java 的包,.net 的命名空間一樣,Typescript 的命名空間可以將代碼包括起來,只對外暴露需要在外部訪問的對象,命名空間內的對象通過 export 導出。1 /** 2 * 通過 namespace 來定義命名空間 3 *分別定義了 A 和 B 兩個命名空間 4 * 我們可以根據需求來調用 A 或 B 里面類和方法 5 */ 6 namespace A { 7 interface Person { 8 name: string, 9 10 work(): void, 11 } 12 13 export class Student implements Person { 14 name: string; 15 16 constructor(name: string) { 17 this.name = name; 18 } 19 20 work() { 21 console.log(`${this.name}在學習`) 22 } 23 } 24 } 25 26 namespace B { 27 interface Person { 28 name: string, 29 30 work(): void, 31 } 32 33 export class Student implements Person { 34 name: string; 35 36 constructor(name: string) { 37 this.name = name; 38 } 39 40 work() { 41 console.log(`${this.name}在寫作業`) 42 } 43 } 44 } 45 46 let a = new A.Student("張三"); 47 a.work(); // 張三在學習 48 let b = new B.Student("李四"); 49 b.work(); // 李四在寫作業
空間命名用起來很簡單,只需要通過 namespace 關鍵字來命名空間即可。
1 /** 2 * 類小黃使其在聲明之前被聲明(緊靠着類聲明) 3 * 類裝飾器應用於類構造函數 4 * 可以用來監視,修改或替換類定義 5 */ 6 function log(params: any) { 7 console.log(params); // ƒ Person() {} 8 params.prototype.name = "動態屬性"; 9 params.prototype.func = () => { 10 console.log("動態方法"); 11 } 12 } 13 14 @log 15 class Person { 16 17 } 18 19 let p: any = new Person(); 20 console.log(p.name); // 動態屬性 21 p.func(); // 動態方法
在上面的代碼中,我們通過 @log 的形式來定義裝飾器,在 log 方法中傳入一個 params 的參數,通過打印我們發現它就是 Person 方法了,那么我們就可以通過原型鏈 prototype 的形式添加屬性和方法了。
上面@log 並沒有傳參,接下來我們看一下在調用裝飾器的時候傳入我們想要的參數:
1 /** 2 * 在 log 方法中,我們通過 return 方式 3 * 將該方法返回,那就跟無參裝飾器一樣了 4 * 這樣我們就可以在 log 方法中傳參了,如 params 5 */ 6 function log(params: string) { 7 return function (target: any) { 8 console.log(params); // 張三 9 console.log(target); // f Person() {} 10 target.prototype.name = params; 11 target.prototype.work = function (): void { 12 console.log(`${params}在工作`) 13 } 14 } 15 } 16 17 @log("張三") 18 class Person { 19 20 } 21 22 let p: any = new Person(); 23 console.log(p.name); // 張三 24 p.work(); // 張三在工作
1 /** 2 * 屬性裝飾器表達式會在運行時當作函數被調用 3 * 傳入下列 2 個參數 4 * 1、對於靜態成員來說是類的構造函數,對於實例成員是類的原型對象 5 * 2、成員的名字 6 */ 7 function log(param: any) { 8 return function (target: any, key: string) { // 分別傳入 2 個參數 9 console.log(param); // 張三 10 console.log(target); // {constructor: ƒ} 11 console.log(key); // name 12 target[key] = param; 13 } 14 } 15 16 17 class Person { 18 @log("張三") 19 name: string; 20 21 constructor() { 22 } 23 24 getName(): void { 25 console.log(this.name); 26 } 27 } 28 29 let p: any = new Person(); 30 console.log(p.name); // 張三 31 p.getName(); // 張三
在上面的代碼中,我們在 name:string 上面添加了 @log 的裝飾器,這就是屬性裝飾器,同時我們對裝飾器進行了傳參“張三”,我們將 log 方法返回,通過打印發現返回的方法的第一個參數 target 為 constructor 構造器,第二個參數為 name,那我們就可以根據 target[key] 的形式對 name 屬性進行賦值了。
1 /** 2 * 方法裝飾器會被應用到方法的屬性描述符上 3 * 用來修改,監視和替換方法定義 4 * 方法裝飾器在運行時傳入下列三個參數 5 * 1、對於靜態成員來說是類的構造函數,對於實例成員來說是類的原型對象 6 * 2、成員的名字 7 * 3、成員的屬性描述符 8 */ 9 function log(param: any) { 10 return function (target: any, key: any, description: any) { // 分別傳入三個參數 11 console.log(param); // 張三 12 console.log(target); // {getName: ƒ, constructor: ƒ} 13 console.log(key); // getName 14 console.log(description); // {writable: true, enumerable: true, configurable: true, value: ƒ} 15 console.log(description.value); // ƒ () {console.log(this.name) } 16 target.age = 20; // 添加屬性 17 target.work = () => console.log(`${param}在工作`); // 添加方法 18 // 修改 getName 方法 19 description.value = ():void => console.log(`${param}---我是被修改的 getName 方法`); 20 } 21 } 22 23 24 class Person { 25 name: string; 26 27 constructor() { 28 } 29 30 @log("張三") 31 getName(): void { 32 console.log(this.name); 33 } 34 } 35 36 let p: any = new Person(); 37 console.log(p.age); // 20 38 p.work(); // 張三在工作 39 p.getName(); // 張三---我是被修改的 getName 方法
4、方法參數裝飾器
1 /** 2 * 方法參數裝飾器會在運行時當中函數被調用 3 * 可以使用參數裝飾器為類的原型增加一些元素數據 4 * 傳入下列 3 個參數 5 * 1、對於靜態成員來說是類的構造函數,對於實例成員來說是類的原型對象 6 * 2、方法的名字 7 * 3、參數在函數參數列表中的索引 8 */ 9 function log(param: any) { 10 return function (target: any, methodName: any, index: any) { // 分別傳入三個參數 11 console.log(param); // name 12 console.log(target); // {getData: ƒ, constructor: ƒ} 13 console.log(methodName); // getData 14 console.log(index); // 0 15 target.age = 18; 16 target.work = (): void => console.log(`我在工作`) 17 } 18 } 19 20 21 class Person { 22 constructor() { 23 } 24 25 getData(@log("name") name: string): void { 26 console.log(name); 27 } 28 } 29 30 let p: any = new Person(); 31 p.getData("張三"); // 張三 32 console.log(p.age); // 18 33 p.work(); // 我在工作
方法參數裝飾器跟其他裝飾器不同的是需要將 @log 寫在方法的參數內,比較怪,一般情況下方法裝飾器就能解決方法參數裝飾器所實現的功能,所以一般我們使用方法參數器即可。
裝飾器的執行順序:屬性裝飾器>方法裝飾器>方法參數裝飾器>類裝飾器,如果有多個相同裝飾器,則先執行后面的裝飾器。
以上就是本人目前學習 Typescript 的一些整理歸納,好記性不如爛筆頭,謹以此記,與君共勉!