TypeScript語法基礎
- 2019 年 10 月 3 日
- 筆記
什麼是TypeScript?
TypeScript是微軟開發的一門程式語言,它是JavaScript的超集,即它基於JavaScript,拓展了JavaScript的語法,遵循ECMAScript規範(ES6/7/8+)。
TypeScript = Type + Script(標準JS),它可以編譯成純JavaScript,已經存在的JavaScript也可以不加改動地在TS的環境上運行。
目前, Angular 已經使用 TypeScript 重構了程式碼,另一大前端框架 Vue 的3.0版本也將使用 TypeScript 進行重構。在可預見的未來,TypeScript 將成為前端開發者必須掌握的開發語言之一。
為什麼要使用TypeScript?
- 提升開發效率。
- 提升可維護性。
- 提升線上運行品質。TS有編譯期的靜態檢查,加上IDE的智慧糾錯,儘可能的將BUG消滅在編譯器上,線上運行時品質更穩定可控。
- 可讀性強,適合團隊協作
TypeScript開發環境
npm install -g typescript // 安裝ts編譯器 tsc hello.ts // 手動編譯ts文件,會生成同名js文件 tsc --init // 生成tsconfig.js文件
當然,我們可以配置webpack,開啟node服務,進行熱更新開發。
TypeScrip數據類型
學習數據類型前,要先明白兩個概念:
強類型和弱類型
強類型指一個變數一旦聲明,就確定了它的類型,此後不能改變它的類型。弱類型可以隨便轉換。TypeScript是強類型語言,JavaScript是弱類型語言。
靜態類型和動態類型
靜態類型語言:編譯階段檢查所有數據的類型。動態類型語言:將檢查數據類型的工作放在程式的執行階段,也就是說,只有在程式執行時才能確定數據類型。
基本類型
在ES6的基礎上,新增了void、any、never、元組、枚舉、高級類型。
布爾、數字、字元串
let bool: boolean = true; let num: number = 10; let str: string = "abc"; let abc: number | boolean | null; // 可以為一個變數聲明多種類型,除非有特殊需求,一般不建議這樣做。
另外,ts字元串有一些特性:
1.多行字元串 ·aa bb cc· 反引號包著,直接換行即可,編譯成js後,會加上n換行符 2.自動拆分字元串 在調用函數時,可以將模板字元串拆分,並且不需要寫小括弧 function test(a:string,b:string,c:number){console.log(a,b,c)} test`string...${name}...${age}...` // 注意調用方式,沒有括弧,直接反引號 會將字元串拆分為第一個參數,name拆分成第二個參數,age為第三個參數
數組
TypeScript的數組,所有元素只能是同一種數據類型。
let arr1: number[] = [1,2,3]; let arr2: Array<number> = [4,5,6]; // 數組的第二種聲明方式,與前面等價 let arr3: string[] = ["hello","array","object"];
元組
元組是特殊的數組,限制了元素的個數和類型。
let tuple: [number,string,boolean] = [10,"hello",true]; // tuple.push(5); // 元組越界,但不會報錯。原則上不能對tuple push,這應該是一個缺陷 // console.log(tuple[3]); // 新增的元素無法訪問。
函數
- 函數的聲明定義(三種方式)
- 函數傳參
- 可選參數必須放在必選參數的後面
- 使用ES6的默認參數,不需要聲明類型
- 使用ES6的剩餘參數,需要聲明類型
// 方式一 箭頭函數:聲明和定義分開 let compute: (x:number, y:number) => number; // 函數聲明,規定了傳入參數、返回值的數據類型 compute = (a, b) => a+b; // 函數定義時,參數名稱不必與聲明時的相同 // 方式二:箭頭函數:聲明的同時定義 let add = (x:number, y:number) => { return x+y }; // 返回值的類型可省略,這利用了ts的類型推斷功能 // 方式三:function關鍵字:聲明和定義分開 function add (x: number,y: number): number; // 方式四:function關鍵字:聲明的同時定義 function add (x: number,y: number): number{ retrun x+y; } //函數傳參: function add789(x: number, y?: number, z=1, ...rest: number[]) { console.log(rest); return y ? x+y : x }
對象
// 正確的寫法 let obj1: {x:number, y:number} = {x: 1, y: 2}; obj1.x = 3; // 不建議的寫法 let obj: object = {x: 1, y: 2}; obj.x = 3; // 報錯,因為定義的時候繞過了聲明檢查,此時不知道是什麼類型。
symbol
let s1: symbol = Symbol(); let s2 = Symbol();
undefind、null
let un: undefined = undefined; let nu: null = null; nu = null; // 這樣是允許的,需要將tsconfig中“strictNullChecks”置為false un = undefined;
void
是一種操作符,可以讓任意一個表達式返回undefined。之所以引進void,是因為undefined不是一個保留字,可以在局部作用域內將其覆蓋掉。
let noReturn = () => {};
any
any 表示變數可以為任何類型,在TS中一般不用它。如果使用它,也便失去了使用TS的意義,與前面不建議為變數聲明多種類型是一個道理。
let x: any; x = 1; x = "str";
never
表示永遠不會有返回值的類型
let error = () => { throw new Error("errors") }; let endless = () => { while(true) {} }; // 以上兩個例子永遠不會有返回值
枚舉類型 enum
枚舉主要來定義一些常量,方便記憶,減少硬編碼,增加可讀性。
基本使用:
// 數字枚舉 enum Role { Reporter, // 默認從0開始 Developer=5, // 也可指定某個值 Maintainer, } console.log(Role); // 可以看到數據結構,能夠進行反向映射,即通過值來訪問成員 console.log(Role.Developer); // 訪問枚舉成員 // 字元串枚舉 不可以進行反向映射 enum message { success = "成功了", fail = "失敗了" } console.log(message); // 異構枚舉,將數字和字元串混用 enum Answer { N = 0, Y = "yes" }
※ 注意:不能修改枚舉成員的值
枚舉成員的分類:
枚舉成員的分類: (1)常量枚舉成員 (2)對已有枚舉成員的引用 (3)常量表達式 (4)非常量表達式。這種成員的值不會在編譯階段確定,在執行階段才會有 例: enum Char { a, b = 9, c = message.success, d = b, e = 1 + 3, f = Math.random(), g = "123".length, } console.log(Char);
常量枚舉和枚舉類型
// 常量枚舉 用const聲明的枚舉都是常量枚舉。特性:在編譯階段被移除,編譯後不會出現 // 作用:當我們不需要對象,只需要對象的值的時候 const enum Month{ Jan, Feb, Mar, } let month = [Month.Jan,Month.Feb,Month.Mar]; console.log(month); // 枚舉類型 枚舉可以作為一種類型 let e: Role = 2; let e1: Role.Developer = 12; // 數字枚舉類型與number類型相互兼容,因此可以複製 console.log(e,e1); // 按照Role的類型去聲明新變數 let g1: message.success = "hello"; // 報錯,字元串枚舉類型message.success與string類型不兼容 e === e1; // 可比較 e === g1; // 不可比較,因為類型不一樣
interface介面
介面可以用來約束對象、函數、類的結構和類型,是一種契約,並且聲明之後不可改變。
1.定義 (interface關鍵字)
interface List { id: number; name: string; } interface Result { data: List[]; // 表示由List介面組成的數組 } function render(result:Result) { result.data.forEach((value)=>{ console.log(value); }) } let result = { data:[ {id:1,name:"a"}, {id:2,name:"b"}, ], }; render(result);
2.內部規範了什麼?
通過上述例子,看到介面規範了成員名稱、成員的的類型、值的類型。
此外,還可以規範成員屬性。
3.成員屬性
可選屬性和只讀屬性
interface List { readonly id: number; // readonly表示只讀屬性 name: string; age?: number; // ?表示可選屬性 }
4.索引簽名
當不確定介面中有多少屬性的時候,可以用索引簽名。
格式:[x: attrType]: valueType 分別規定了成員的類型和值的類型,即通過什麼來索引和訪問的值的類型。
一般通過數字和字元串來索引,也可以兩者混合索引。
// 用數字索引 interface StringArray { [index: number]: string; // 表示,用任意的數字去索引StringArray,都會得到一個string。這就相當於聲明了一個字元串類型的數組 } let chars: StringArray = ["A","B"]; // 此時,chars就是一個字元串數組,我們可以用下標去訪問每個元素 console.log(chars,chars[0]); // 用字元串和數字混合索引 interface Names { [x: string]: string; // 用任意的字元串去索引Names,得到的結果都是string。 // y: number; // 此時不能聲明number類型的成員 // [y: number]: number // 報錯,因為x和y的值string和number類型不兼容 [z: number]: any; // 兩個簽名的返回值類型之間要相互兼容。為了能保持類型的兼容性。 } let names: Names = {"ming":"abc",1:"45"}; console.log(names[1],names["ming"]); // 通過數字索引、通過字元串索引
※ 注意值的類型要兼容
(1)索引簽名和普通成員
如果設置了[x: string]: string,不能再設置y: number。如果設置了[x: string]: number不能再設置y: string
(2)索引簽名和索引簽名
如果多個索引簽名的值不同,要注意相互兼容,比方any和string
5.函數傳參時如何繞過類型檢查
如果在接收的後端數據中,比約定好的介面多了一個欄位,能否通過類型檢查?會不會報錯?
let result = { data:[ {id:1,name:"a",sex:"man"}, {id:2,name:"b"}, ], }; render(result); // 這樣是不會報錯的,只要滿足介面約定的必要條件即可 render({ data:[ {id:1,name:"a",sex:"man"}, {id:2,name:"b"}, ], }); // 但如果這樣調用,會報錯,因為無法通過sex:"man"的類型檢查。這時候需要用其他方法
我們有三種方法:
- 通過介面定義變數,函數調用時傳入變數名(只對必要的約定條件進行檢查,多餘的數據不做檢查)
- 類型斷言(所有約定都不做類型檢查,失去了ts類型檢查的意義)
- 索引簽名
第一種方法已經在上面做了示例,我們看後面兩種方法如何做:
// 類型斷言 render({ data:[ {id:"b",name:3,sex:"man"}, {id:2,name:"b"}, ], }as Result); // 明確告訴編譯器,數據符合Result,這樣,編譯器會繞過類型檢查 render(<Result>{ data:[ {id:1,name:"a",sex:"man"}, {id:2,name:"b"}, ], }); // 與上等價,但在React中容易引起歧義。不建議使用 // 索引簽名 interface List { id: number; name: string; [x: string]: any; // 字元串索引簽名。用任意字元串去索引List,可以得到任意的結果,這樣List介面可以支援多個未知屬性 }
在什麼場景下用什麼方法,需要我們熟知這三種方法的特性
6.介面和函數
介面可以用來定義函數的傳參、返回值的類型
interface Add1 { (x: number,y: number): number; } let add1: Add1 = (a,b) => a+b;
此外,還可以用類型別名來定義函數
type Add2 = (x: number,y: number) => number; let add2: Add2 = (a,b) => a+b; // 聲明+定義
我們再來總結一下函數的聲明定義方式:
- 普通聲明定義(function、箭頭函數)
- 介面定義類型
- 類型別名
另外,介面內也可以定義函數
// 混合類型介面 interface Lib { abc(): void; version: string; doSomething(): void; } function getLib(){ let lib: Lib = { abc: ()=>{}, version: "1.0", doSomething: ()=>{} }; // let lib: Lib = {} as Lib; // 定義的時候,這種方式更方便 lib.version = "1.0"; lib.doSomething = () => {}; return lib; } let lib1 = getLib(); console.log(lib1,lib1.version,lib1.doSomething());
class類
關於類的成員:
1.屬性必須有類型註解
2.屬性必須有初始值
3.屬性修飾符:
(1)公有 public
所有成員默認都是public。可以通過各種方式訪問。
(2)私有 private
私有成員只能在類中被訪問,不能被實例和子類訪問。如果給構造函數加上私有屬性,表示這個類既不能被實例化也不能被繼承。
(3)受保護 protected
受保護成員只能在類和子類中訪問,不能通過它們的實例訪問。如果給構造函數加上受保護屬性,表示這個類不能被實例化只能被繼承。也就是聲明了一個基類。
(4)靜態 static
靜態成員只能通過類名和子類名訪問,不能被實例訪問。
(5)只讀 readonly
只讀成員不能被修改。
4.以上屬性除了可以修飾成員,也可以修飾構造函數中的參數(static除外)。這樣可以省去構造函數之中外的類型註解,簡化程式碼。
class Dog{ constructor(name: string){ this.name = name; // 屬性必須賦初值 } name: string; // 必須要為屬性添加類型註解。 run(){ console.log("running"); this.pri(); // 只能在類內部訪問私有成員 this.pro(); } private pri(){ // 私有成員只能被類本身調用,不能被類的實例和子類調用 console.log("pri是dog類的私有屬性"); } protected pro(){ // 受保護成員只能在類和子類中訪問 console.log("pro是dog類的受保護屬性"); } readonly logs: string = "new"; static food: string = "food"; // 靜態修飾後,只能通過類名調用,不能被子類和實例調用。靜態成員可以被繼承 } let dog = new Dog("dog1"); dog.run(); console.log(Dog.food); // 通過類名訪問靜態成員
抽象類和多態
所謂抽象類(abstract),是只能被繼承,不能被實例化的類。
抽象方法
在抽象類中,不必定義方法的具體實現,這就構成了抽象方法。在抽象類中使用abstratct關鍵字修飾後,不能定義該方法的具體實現。
抽象方法的好處是:實現多態。
interface AnimalParam{ bigClass: string, environment: string, [x: string]: string; } abstract class Animal{ // 抽象類用abstract關鍵字修飾 constructor(params: AnimalParam) { // 構造函數參數使用介面是為了其子類在定義的時候方便傳參 this.bigClass = params.bigClass; this.environment = params.environment; } bigClass: string; environment: string; abstract sleep(): void; } class Dogs extends Animal{ constructor(props: AnimalParam){ super(props); this.name = props.name; } name: string; run(){ console.log("running"); } sleep() { console.log("dog sleep") } } class Cat extends Animal{ sleep(): void { console.log("cat sleep") } } let dog1 = new Dogs({bigClass:"a",environment:"b",name:"xiaoqi"}); let cat = new Cat({bigClass:"a",environment:"b"}); let animals: Animal[] = [dog1,cat]; animals.forEach(i=>{ i.sleep(); });
this
我們可以在方法中返回this,可以進行鏈式調用,非常方便。
class WorkFlow{ step1(){ return this; } step2(){ return this; } } class Myflow extends WorkFlow{ next(){ return this; } } console.log(new WorkFlow().step1().step2()); console.log(new Myflow().next().step1().step2());
類和介面
類類型介面
- 介面約束類成員有哪些屬性,以及它們的類型
- 類在實現介面約束的時候,必須實現介面中描述的所有內容。可以多,不可以少
- 介面只能約束類的公有成員,即不可以將介面中規定的成員置為非公有的屬性
- 介面不能約束類的構造函數
interface Human { name: string; eat(): void; } class Asian implements Human{ constructor(name: string){ this.name = name; } name: string; eat(){} sleep(){} }
介面繼承介面
interface Man extends Human{ run(): void } interface Child { cry(): void } interface Boy extends Man,Child{} // 多繼承,將多個介面合併成一個介面
介面繼承類
可以理解為,將類轉化成介面。介面集成類的時候,不僅抽離了公有成員,也抽離了私有成員、受保護成員。
如何理解呢?這麼做的目的是限定介面的使用範圍,並不會真正為這個介面添加類的私有和受保護屬性,而這個限定範圍就是:只能由子類來實現這個介面。
class Auto{ state = 1; private state2 = 0; protected state3 = 3; } interface AutoInterface extends Auto{} // 介面繼承類 class C implements AutoInterface{ // C在實現這個介面的時候,無法實現介面中的私有成員和受保護成員,因此報錯 state = 1 } class Bus extends Auto implements AutoInterface{ // Auto的子類Bus,遵循AutoInterface介面 showMsg(){ // console.log(this.state2); console.log(this.state3); } } let bus = new Bus(); bus.showMsg(); console.log(bus);