TypeScript是JavaScript類型的超集,它可以編譯成純JavaScript。
TypeScript可以在任何瀏覽器、任何計算機和任何操作系統上運行,並且是開源的。
什么是 TypeScript
TypeScript 是 JavaScript 的、帶有類型的超集,並且能夠編譯成普通的 JavaScript。
編譯:
類型:
超集:
TypeScript 本身支持所有 JavaScript 的語法,並在此基礎上添加了額外的功能和特性。
TypeScript 是由微軟開發的一款開源的編程語言。
TypeScript 是 Javascript 的超集,遵循最新的 ES6、Es5 規范。TypeScript 擴展了 JavaScript 的語法。
TypeScript 更像后端 java、C#這樣的面向對象語言,可以讓 js 開發大型企業項目。
谷歌也在大力支持 Typescript 的推廣,谷歌的 angular2.x+就是基於 Typescript 語法。
最新的 Vue 、React 也可以集成 TypeScript。
Nodejs 框架 Nestjs、midway 中用的就是 TypeScript 語法。
帶有類型,是說js在定義變量的時候,類型是動態的,只有在運行的時候才能知道它的具體類型,比如 number 或者 string,並且類型也是可以動態變化的,而 TypeScript 則是要求變量有確定的類型,並且在編寫代碼的時候就已經確定,如果把字符串賦給類型為 number ,數字類型的變量,就會出錯。
為什么用 TypeScript
在 stackoverflow 發起的2020年程序員調查中,TypeScript 在程序員最愛的編程語言中排在了第二位
https://stackoverflow.blog/2020/05/27/2020-stack-overflow-developer-survey-results/
好處優勢:
類型檢查、代碼補全、易於維護、入門簡單
之所以大家喜歡 TypeScript,是因為:
- TypeScript 有類型檢查機制,我們可以在寫代碼的時候就能夠發現錯誤,比如給函數誤傳了類型不同的參數,那么通過 VS Code 對 TypeScript 的強力支持,我們能立刻看到錯誤。
- 另外 VS Code 能根據 TypeScript 的類型信息提供更好的代碼提示和補全功能。
此外,對於大型項目、多人協作編寫代碼時,類型起到了文檔的作用,可以清楚的知道我這個變量是什么類型,或者我定義的函數需要什么樣的參數,我的對象里又有哪些屬性。這樣讓代碼更易於維護,這也是為什么大公司、大型項目更偏愛 TypeScript - 最后 TypeScript 入門的門檻低,只要你會 JavaScript,那么你就已經能編寫 TypeScript 代碼了。另外因為 JS 的快速發展,好多以前在 typescript 才能用的功能,你可能在JS 里已經用到了,所以說要學習的東西就更少了。
除了這些好處之外,它也有其他靜態類型語言比如 Java/c++ 的通病,就是代碼量會增加,並且有時候類型過於復雜反而使得代碼顯的更難閱讀,不過跟它帶來的優勢相比,也顯得不那么突出了。
安裝TypeScript
有兩種主要的方式來獲取TypeScript工具:
- 通過npm(Node.js包管理器)
- 安裝Visual Studio的 TypeScript 插件
Visual Studio 2017和Visual Studio 2015 Update 3默認包含了TypeScript。 如果你的Visual Studio還沒有安裝TypeScript,你可以下載它。
針對使用npm的用戶:
npm install -g typescript
構建你的第一個TypeScript文件
在編輯器,將下面的代碼輸入到greeter.ts
文件里:
編譯代碼
我們使用了.ts
擴展名,但是這段代碼僅僅是JavaScript而已。 你可以直接從現有的JavaScript應用里復制/粘貼這段代碼。
在命令行上,運行TypeScript編譯器:
tsc greeter.ts
然后可以看到,輸出結果為一個 greeter.js
文件,它包含了和輸入文件中相同的JavsScript代碼。
// greeter.js function greeter(person) { return "Hello, " + person; } var user = "Jane User"; document.body.innerHTML = greeter(user);
一切准備就緒,我們可以運行這個使用 TypeScript 寫的 JavaScript 應用了!
接下來讓我們看看 TypeScript 工具帶來的高級功能。 給 person
函數的參數添加 :string
類型注解,如下:
function greeter(person: string) { return "Hello, " + person; } let user = "Jane User"; document.body.innerHTML = greeter(user);
運行編譯:
tsc greeter.ts
可以看到 greeter.js 的代碼沒變。
類型注解
TypeScript里的類型注解是一種輕量級的為函數或變量添加約束的方式。 在這個例子里,我們希望 greeter
函數接收一個字符串參數。
然后嘗試把 greeter
的調用改成傳入一個數組:
function greeter(person: string) { return "Hello, " + person; } let user = [0, 1, 2]; document.body.innerHTML = greeter(user);
可以看到報錯:
重新運行編譯也可以看到報錯:
類似地,嘗試刪除 greeter
調用的所有參數。 TypeScript會告訴你使用了非期望個數的參數調用了這個函數。 在這兩種情況中,TypeScript提供了靜態的代碼分析,它可以分析代碼結構和提供的類型注解。
要注意的是盡管有錯誤,greeter.js
文件還是被創建了。 就算你的代碼里有錯誤,你仍然可以使用TypeScript。但在這種情況下,TypeScript會警告你代碼可能不會按預期執行。
新建一個 index.ts 文件:
let a:number = 10;
console.log(a);
編譯並運行:可以看到
另一種編譯方法是通過 deno:deno 本身就支持 TypeScript ,所以只需要安裝 deno 的運行環境就可以了。
// 使用 Shell: curl -fsSL https://x.deno.js.cn/install.sh | sh // 使用 PowerShell: iwr https://x.deno.js.cn/install.ps1 -useb | iex
安裝完成后,輸入:
deno run index.ts
也可以輸出結果:10
打開 index.js 可以看到:除了去除了類型沒有什么變化
var a = 10; console.log(a);
有個問題:JavaScript 版本那么多,tsc 怎么知道要編譯成那個版本呢?
tsc 默認會編譯成 ES3 版。
下面我們寫一個 async 函數:
let a:number = 10; console.log(a); async function func() { }
再重新編譯下,然后打開 index.js 文件,發現生成了很多復雜的代碼來支持 async。
那如果我想要生成 ES2017 的代碼呢?
需要在根目錄下創建一個 tsconfigt.json 文件:
{ "compilerOptions": { "target": "ES2017" } }
這時編譯可以輸入:
tsc
因為有了 tscofigt.josn 文件后,這個文件夾會自動成為 TypeScript 項目,tsc 會自動找到 .ts 文件,並進行編譯。如果指定了文件名,那么 tscofigt.josn 設置就會被忽略。
然后打開 index.js 文件可以看到:
let a = 10; console.log(a); async function func() { }
還可以用開發工具 Vscode 自動編譯.ts 文件
在終端中輸入:可以生成配置文件 tsconfig.json
tsc --init
注意:如果你已經創建過了,就不會再生成了。
TypeScript 基本語法
布爾類型(boolean)、數字類型(number)、字符串類型(string)、數組類型(array)、元組類型(tuple)、枚舉類型(enum)、任意類型(any)、任意類型(any)、對象類型(object)、void類型、never類型、組合類型、類型別名、null 和 undefined
1、基本類型
給變量定義類型有兩種方式:隱式類型和顯式類型
隱式類型:是由 TypeScript 根據變量的值來推斷類型,代碼寫法和js一樣,但不同的是:后面不能用其他類型的值來給它重新賦值。
比如:
let a = 10;
a = "hello";
可以看到a上報錯,鼠標移到a上可以看到:
顯式類型:和之前運行的ts代碼示例一樣,用 : + 類型 來規定這個變量是什么類型的。
·常用類型:
布爾類型(boolean)、數字類型(number)、字符串類型(string)、null 和 undefined
例如:
let b: boolean = true;
你還可以使用模版字符串,它可以定義多行文本和內嵌表達式。 這種字符串是被反引號包圍( `),並且以 ${ expr } 這種形式嵌入表達式
let username: string = `Gene`; let age: number = 37; let sentence: string = `Hello, my name is ${ username }. I'll be ${ age + 1 } years old next month.`; console.log(sentence); // Hello, my name is Gene. I'll be 38 years old next month.
這與下面定義sentence的方式效果相同:
let username: string = `Gene`; let age: number = 37; let sentence: string = "Hello, my name is " + username + ".\n" + "I'll be " + (age + 1) + " years old next month."; console.log(sentence); // Hello, my name is Gene. // I'll be 38 years old next month.
·任意類型 any
如果想讓一個變量可以是任何類型,就像 js 中可以任意更改的話。那么我們可以把它的類型定義為 any。
例如:這時可以看到不會報錯
let a: any = 10;
a = "hello";
類型也可以用在函數的參數和返回值中
比如:
function add(a: number, b: number): number { return a + b; } add(1, 2); // 小括號后面的:number 是返回值的類型,當然也可以省略不寫 function add(a: number, b: number) { return a + b; } add(1, 2);
注意:調用函數時必須傳遞跟參數列表相同的參數(也就是說實參和形參的數量要一致),不像 js 可以不傳或者只傳前面的幾個參數。
例如:我們只傳a的值
function add(a: number, b: number): number{ return a + b; } add(1);
會報錯:
·void類型
某種程度上來說,void
類型像是與 any
類型相反,它表示沒有任何類型。 如果函數不返回值的話,可以使用 :void 類型,代表函數沒有返回值。
例如:
function add(a: number, b: number): void { console.log(a + b); }
2、組合類型
如果一個變量可以有多個類型,但是又不想使用 any 破壞類型檢查,那么可以使用組合類型。組合類型使用 | 操作符來定義。
例如:
let a :number | string = 10;
a = "hello";
不過這樣代碼看起來不太方便,並且這個組合類型只能給 a 使用,如果再有一個變量 b,也可以是number 或者 string 類型,那么還需要再重復定義這個類型。
要解決這個問題,我們可以使用 type 關鍵字,來給這個組合類型起個別名,讓代碼更易讀,也方便其他變量使用。
例如:
type NumStr = number | string; let a: NumStr = 10; a = "hello"; let b: NumStr = '123'; b = 123;
另外組合類型也可以直接使用字面值來定義,這樣就規定了一個變量的取值范圍。
例如:
let c: "on" | "off" = "on"; c = "off"; c = "other";
可以看到最后一條報錯:
3、對象類型
使用 interface 接口,接口是用來規范一個對象里應該都有哪些屬性,包括它的名字和類型
例子:如果有一個 post 文章變量,里面有 title 和 author 屬性,並且都是 string 類型的。那么我們可以使用接口來定義一個 post 類型。
interface Post { title: string; author: string; } let post: Post = { title: '標題', author: '作者' }
如果在增加一個屬性就會報錯:
同樣,去掉 title 屬性,一樣會報錯:
接口除了可以檢查對象是否符合規范外,也可以用於函數參數的類型檢查
注意:如果傳遞進來的對象沒有定義類型的話,只要它的屬性里包括接口中定義的規范,那么就可以通過檢查,哪怕它有額外的屬性。
例如:
interface Post { title: string; author: string; } function getTitle(post: Post) { console.log(post.title) } let post = { title: '標題', author: '作者', publishDate: '2020-10-10' } getTitle(post); // 標題
如果想要嚴格檢查對象參數的話,可以像之前那樣把 post 對象定義為 Post 接口類型的:
let post: Post = {
}
或者直接給函數傳遞對象字面值:
getTitle({ title: '標題', author: '作者', }); // 標題
4、數組類型
給數組規定類型,可以保證里面的元素都是同一個類型。以防在統一處理數組元素時,會混進來其他類型的元素,導致異常;或者防止意外給數組元素賦了其他類型的值。
let arr: number[] = [1, 2, 3];
還有一種寫法是:使用數組泛型 Array<元素類型>
:
let arr: Array<number> = [1, 2, 3];
當然也可以使用 any 定義任意類型的數組:
let arr: any[] = [1, "fh", true];
5、元祖 (tuple)
元組類型允許表示一個已知元素數量和類型的數組,各元素的類型不必相同。
有限元素數量的數組,每個元素要分別指定是什么類型
let tup: [number, string, boolean] = [1, "fh", true];
6、枚舉類型(enum)
enum
類型是對 JavaScript 標准數據類型的一個補充。 像 C# 等其它語言一樣,使用枚舉類型可以為一組數值賦予友好的名字。
如果能在程序中用自然語言中有相應含義的單詞來代表某一狀態,則程序就很容易閱讀和理解。也就是說,事先考慮到某一變量可能取的值,盡量用自然語言中含義清楚的單詞來表示它的每一個值,這種方法稱為枚舉方法,用這種方法定義的類型稱枚舉類型。
enum Status {success=1, error=2}; let s:Status = Status.success; console.log(s); //1
enum Color {blue, red, orange}; let c:Color = Color.red; console.log(c); //1 如果標識符沒有賦值 它的值就是下標
枚舉類型提供的一個便利是你可以由枚舉的值得到它的名字。 例如,我們知道數值為2,但是不確定它映射到 Color 里的哪個名字,我們可以查找相應的名字:
enum Color {blue , red, orange} let colorName1: string = Color[2]; console.log(colorName1); // orange enum NewColor {blue = 1, red, orange} let colorName2: string = NewColor[2]; console.log(colorName2); // // 顯示'red'因為上面代碼里它的值是2
7、對象類型(object)
object
表示非原始類型,也就是除number
,string
,boolean
,symbol
,null
或undefined
之外的類型。
使用object
類型,就可以更好的表示像Object.create
這樣的API。例如:
declare function create(o: object | null): void; create({ prop: 0 }); // OK create(null); // OK
let arr2:object = [11, 'asd', 1321]; console.log(arr2);
8、null 和 undefined:其他(never類型)數據類型的子類型
TypeScript里,undefined
和 null
兩者各自有自己的類型分別叫做 undefined
和 null
。 和 void
相似,它們的本身的類型用處不是很大:
let num:number; console.log(num) //輸出:undefined 報錯 let num1:undefined; console.log(num1) //輸出:undefined //正確 let num2:number | undefined; num2 = 123; console.log(num2);
默認情況下 null 和 undefined 是所有類型的子類型。 就是說你可以把 null 和undefined 賦值給 number 類型的變量。
然而,當你指定了--strictNullChecks 標記,null和undefined只能賦值給void和它們各自。 這能避免 很多常見的問題。 也許在某處你想傳入一個 string或null或undefined,你可以使用聯合類型string | null | undefined。 再次說明,稍后我們會介紹聯合類型。
注意:我們鼓勵盡可能地使用--strictNullChecks,但在本手冊里我們假設這個標記是關閉的。
9、never類型
never
類型表示的是那些永不存在的值的類型。 例如, never
類型是那些總是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型; 變量也可能是 never
類型,當它們被永不為真的類型保護所約束時。
never
類型是其他類型 (包括 null 和 undefined)的子類型,代表從不會出現的值。這意味着聲明 never 的變量只能被 never 類型所賦值。 即使 any
也不可以賦值給 never
。
let a: never; a = 123; //錯誤寫法 a = (function () { throw new Error("錯誤") })(); // 返回never的函數必須存在無法達到的終點 function error(message: string): never { throw new Error(message); } // 推斷的返回值類型為never function fail() { return error("Something failed"); } // 返回never的函數必須存在無法達到的終點 function infiniteLoop(): never { while (true) { } }
類型斷言
有時候你會遇到這樣的情況,你會比TypeScript更了解某個值的詳細信息。 通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。
通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。 類型斷言好比其它語言里的類型轉換,但是不進行特殊的數據檢查和解構。 它沒有運行時的影響,只是在編譯階段起作用。 TypeScript會假設你,程序員,已經進行了必須的檢查。
類型斷言有兩種形式。 其一是“尖括號”語法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
另一個為 as
語法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
兩種形式是等價的。 至於使用哪個大多數情況下是憑個人喜好;然而,當你在TypeScript里使用JSX時,只有 as
語法斷言是被允許的。
關於let
你可能已經注意到了,我們使用 let
關鍵字來代替大家所熟悉的JavaScript關鍵字 var
。 let
關鍵字是JavaScript的一個新概念,TypeScript實現了它。 我們會在以后詳細介紹它,很多常見的問題都可以通過使用 let
來解決,所以盡可能地使用let
來代替var
吧。
變量聲明
let和const是JavaScript里相對較新的變量聲明方式。 像我們之前提到過的, let在很多方面與var是相似的,但是可以幫助大家避免在JavaScript里常見一些問題。 const是對let的一個增強,它能阻止對一個變量再次賦值。
因為TypeScript是JavaScript的超集,所以它本身就支持let和const。 下面我們會詳細說明這些新的聲明方式以及為什么推薦使用它們來代替 var。
var 聲明
一直以來我們都是通過 var 關鍵字定義JavaScript變量。
function f() { var a = 10; return function g() { var b = a + 1; return b; } } var g = f(); g(); // returns 11;
上面的例子里,g
可以獲取到f
函數里定義的a
變量。 每當 g
被調用時,它都可以訪問到f
里的a
變量。 即使當 g
在f
已經執行完后才被調用,它仍然可以訪問及修改a
。
function f() { var a = 1; a = 2; var b = g(); a = 3; return b; function g() { return a; } } f(); // returns 2
作用域規則
對於熟悉其它語言的人來說,var
聲明有些奇怪的作用域規則。 看下面的例子:
function f(shouldInitialize: boolean) { if (shouldInitialize) { var x = 10; } return x; } f(true); // returns '10' f(false); // returns 'undefined'
變量 x
是定義在*if
語句里面*,但是我們卻可以在語句的外面訪問它。 這是因為 var
聲明可以在包含它的函數,模塊,命名空間或全局作用域內部任何位置被訪問(我們后面會詳細介紹),包含它的代碼塊對此沒有什么影響。 有些人稱此為 var
作用域或函數作用域*。 函數參數也使用函數作用域。
這些作用域規則可能會引發一些錯誤。 其中之一就是,多次聲明同一個變量並不會報錯:
function sumMatrix(matrix: number[][]) { var sum = 0; for (var i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (var i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum; }
這里很容易看出一些問題,里層的for循環會覆蓋變量i,因為所有i都引用相同的函數作用域內的變量。 有經驗的開發者們很清楚,這些問題可能在代碼審查時漏掉,引發無窮的麻煩。
捕獲變量怪異之處
快速的猜一下下面的代碼會返回什么:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); }
好吧,看一下結果:
10 10 10 10 10 10 10 10 10 10
很多JavaScript程序員對這種行為已經很熟悉了,但如果你很不解,你並不是一個人。 大多數人期望輸出結果是這樣:
0 1 2 3 4 5 6 7 8 9
還記得我們上面提到的捕獲變量嗎?
我們傳給 setTimeout 的每一個函數表達式實際上都引用了相同作用域里的同一個 i。
讓我們花點時間思考一下這是為什么。 setTimeout
在若干毫秒后執行一個函數,並且是在 for
循環結束后。 for
循環結束后,i
的值為 10
。 所以當函數被調用的時候,它會打印出 10
!
一個通常的解決方法是使用立即執行的函數表達式(IIFE)來捕獲每次迭代時 i
的值:
for (var i = 0; i < 10; i++) { // capture the current state of 'i' // by invoking a function with its current value (function(i) { setTimeout(function() { console.log(i); }, 100 * i); })(i); }
這種奇怪的形式我們已經司空見慣了。 參數 i
會覆蓋 for
循環里的 i
,但是因為我們起了同樣的名字,所以我們不用怎么改for
循環體里的代碼。
let
聲明
現在你已經知道了 var
存在一些問題,這恰好說明了為什么用 let
語句來聲明變量。 除了名字不同外, let
與 var
的寫法一致。
主要的區別不在語法上,而是語義,我們接下來會深入研究。
塊作用域
當用 let 聲明一個變量,它使用的是詞法作用域或塊作用域。 不同於使用 var 聲明的變量那樣可以在包含它們的函數外訪問,塊作用域變量在包含它們的塊或for循環之外是不能訪問的。
function f(input: boolean) { let a = 100; if (input) { // Still okay to reference 'a' let b = a + 1; return b; } // 報錯 Error: 'b' doesn't exist here return b; }
這里我們定義了2個變量 a
和 b
。 a
的作用域是 f
函數體內,而 b
的作用域是 if
語句塊里。
在 catch
語句里聲明的變量也具有同樣的作用域規則。
try { throw "oh no!"; } catch (e) { console.log("Oh well."); } // 報錯 Error: 'e' doesn't exist here console.log(e);
擁有塊級作用域的變量的另一個特點是,它們不能在被聲明之前讀或寫。 雖然這些變量始終“存在”於它們的作用域里,但在直到聲明它的代碼之前的區域都屬於 暫時性死區。 它只是用來說明我們不能在 let
語句之前訪問它們,幸運的是TypeScript可以告訴我們這些信息。
注意一點,我們仍然可以在一個擁有塊作用域變量被聲明前獲取它。 只是我們不能在變量聲明前去調用那個函數。 如果生成代碼目標為ES2015,現代的運行時會拋出一個錯誤;然而,現今TypeScript是不會報錯的。
function foo() { // okay to capture 'a' return a; } // 不能在'a'被聲明前調用'foo' // 運行時應該拋出錯誤 foo(); let a;
重定義及屏蔽
我們提過使用 var
聲明時,它不在乎你聲明多少次;你只會得到1個。
function f(x) { var x; var x; if (true) { var x; } }
在上面的例子里,所有x的聲明實際上都引用一個相同的 x,並且這是完全有效的代碼。 這經常會成為 bug 的來源。 好的是, let 聲明就不會這么寬松了。
let x = 10; let x = 20; // 錯誤,不能在1個作用域里多次聲明`x`
並不是要求兩個均是塊級作用域的聲明TypeScript才會給出一個錯誤的警告。
function f(x) { let x = 100; // error: interferes with parameter declaration } function g() { let x = 100; var x = 100; // error: can't have both declarations of 'x' }
並不是說塊級作用域變量不能用函數作用域變量來聲明。 而是塊級作用域變量需要在明顯不同的塊里聲明。
function f(condition, x) { if (condition) { let x = 100; return x; } return x; } f(false, 0); // returns 0 f(true, 0); // returns 100
在一個嵌套作用域里引入一個新名字的行為稱做屏蔽。 它是一把雙刃劍,它可能會不小心地引入新問題,同時也可能會解決一些錯誤。 例如,假設我們現在用 let 重寫之前的 sumMatrix 函數。
function sumMatrix(matrix: number[][]) { let sum = 0; for (let i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (let i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum; }
這個版本的循環能得到正確的結果,因為內層循環的 i 可以屏蔽掉外層循環的 i。
塊級作用域變量的獲取
在我們最初談及獲取用 var
聲明的變量時,我們簡略地探究了一下在獲取到了變量之后它的行為是怎樣的。 直觀地講,每次進入一個作用域時,它創建了一個變量的 環境。 就算作用域內代碼已經執行完畢,這個環境與其捕獲的變量依然存在。
function theCityThatAlwaysSleeps() { let getCity; if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity(); } console.log(theCityThatAlwaysSleeps()); // Seattle
因為我們已經在 city
的環境里獲取到了 city
,所以就算 if
語句執行結束后我們仍然可以訪問它。
回想一下前面 setTimeout 的例子,我們最后需要使用立即執行的函數表達式來獲取每次 for 循環迭代里的狀態。 實際上,我們做的是為獲取到的變量創建了一個新的變量環境。 這樣做挺痛苦的,但是幸運的是,你不必在TypeScript里這樣做了。
當 let 聲明出現在循環體里時擁有完全不同的行為。 不僅是在循環里引入了一個新的變量環境,而是針對 每次迭代都會創建這樣一個新作用域。 這就是我們在使用立即執行的函數表達式時做的事,所以在 setTimeout 例子里我們僅使用 let 聲明就可以了。
for (let i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); }
會輸出與預料一致的結果:
0 1 2 3 4 5 6 7 8 9
const
聲明
const
聲明是聲明變量的另一種方式。
const numLivesForCat = 9;
它們與 let
聲明相似,但是就像它的名字所表達的,它們被賦值后不能再改變。 換句話說,它們擁有與 let
相同的作用域規則,但是不能對它們重新賦值。
const numLivesForCat = 9; const kitty = { name: "Aurora", numLives: numLivesForCat, } // Error kitty = { name: "Danielle", numLives: numLivesForCat }; // all "okay" kitty.name = "Rory"; kitty.name = "Kitty"; kitty.name = "Cat"; kitty.numLives--; console.log(kitty); // { name: 'Cat', numLives: 8 }
除非你使用特殊的方法去避免,實際上 const 變量的內部狀態是可修改的。 幸運的是,TypeScript允許你將對象的成員設置成只讀的。 接口一章有詳細說明。
let
vs. const
現在我們有兩種作用域相似的聲明方式,我們自然會問到底應該使用哪個。 與大多數泛泛的問題一樣,答案是:依情況而定。
使用最小特權原則,所有變量除了你計划去修改的都應該使用const。 基本原則就是如果一個變量不需要對它寫入,那么其它使用這些代碼的人也不能夠寫入它們,並且要思考為什么會需要對這些變量重新賦值。 使用 const也可以讓我們更容易的推測數據的流動。
解構
解構數組
最簡單的解構莫過於數組的解構賦值了:
let input = [1, 2]; let [first, second] = input; console.log(first); // outputs 1 console.log(second); // outputs 2
這創建了2個命名變量 first 和 second。 相當於使用了索引,但更為方便:
first = input[0];
second = input[1];
解構作用於已聲明的變量會更好:
// 交換變量 [first, second] = [second, first];
作用於函數參數:
function f([first, second]: [number, number]) { console.log(first); console.log(second); } f([1, 2]); // 1 // 2
你可以在數組里使用 ... 語法創建剩余變量:
let [first, ...rest] = [1, 2, 3, 4]; console.log(first); // outputs 1 console.log(rest); // outputs [ 2, 3, 4 ]
當然,由於是JavaScript, 你可以忽略你不關心的尾隨元素:
let [first] = [1, 2, 3, 4]; console.log(first); // outputs 1
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4]; console.log(second); // outputs 2 console.log(fourth); // outputs 4
對象解構
你也可以解構對象:
let o = { a: "foo", b: 12, c: "bar" }; let { a, b } = o; console.log(a); // foo console.log(b); // 12
這通過 o.a
and o.b
創建了 a
和 b
。 注意,如果你不需要 c
你可以忽略它。
就像數組解構,你可以用沒有聲明的賦值:
({ a, b } = { a: "baz", b: 101 });
注意,我們需要用括號將它括起來,因為Javascript通常會將以 {
起始的語句解析為一個塊。
你可以在對象里使用 ...
語法創建剩余變量:
let o = { a: "foo", b: 12, c: "bar" }; let { a, ...passthrough } = o; let total = passthrough.b + passthrough.c.length; console.log(total); // 15 console.log(passthrough); // { b: 12, c: 'bar' }
屬性重命名
你也可以給屬性以不同的名字:
let o = { a: "foo", b: 12, c: "bar" }; let { a: newName1, b: newName2 } = o; console.log(newName1); // foo console.log(newName2); // 12
這里的語法開始變得混亂。 你可以將 a: newName1
讀做 "a
作為 newName1
"。 方向是從左到右,好像你寫成了以下樣子:
let newName1 = o.a;
let newName2 = o.b;
令人困惑的是,這里的冒號不是指示類型的。 如果你想指定它的類型, 仍然需要在其后寫上完整的模式。
let o = { a: "foo", b: 12, c: "bar" }; let {a, b}: {a: string, b: number} = o; console.log(a); // foo console.log(b); // 12
默認值
默認值可以讓你在屬性為 undefined 時使用缺省值:
function keepWholeObject(wholeObject: { a: string, b?: number }) { let { a, b = 1001 } = wholeObject; }
現在,即使 b
為 undefined , keepWholeObject
函數的變量 wholeObject
的屬性 a
和 b
都會有值。
function keepWholeObject(wholeObject: { a: string, b?: number }) { let { a, b = 1001 } = wholeObject; return {a, b}; } console.log(keepWholeObject({a: '11'})); // { a: '11', b: 1001 }
TypeScript中的函數
1、函數的定義
傳遞參數和返回值都要進行指定類型
返回類型必須是 string 類型,不能寫成別的數據類型,否則會報錯
// 有返回值 function run1(): string { return 'run'; } // 無返回值 function run2(): void{ console.log('run'); } // 箭頭函數 const materials = [ 'Hydrogen', 'Helium', 'Lithium', 'Beryllium' ]; console.log(materials.map((material): any => material.length)); // [8, 6, 7, 9] 加不加any都可以
匿名函數的寫法:
var run2=function():string{//指定返回值類型為string字符串類型 return '1243'; } run2(); //調用方法 console.log(run2()); // 1243
定義方法中的傳參:
function getInfo(name: string, age: number): string { return `${name} --- ${age}`; } console.log(getInfo("zhangsan", 20)); // zhangsan --- 20
2、可選參數
es5 里面方法的實參和行參可以不一樣,但是 ts 中必須一樣,如果不一樣就需要配置可選參數,用問號?表示
如果調用函數的時候,不傳第二個參數,則要在第二個函數 age?上加一個問號。問號就表示這個age可以傳,可以不傳。
function getInfo(name: string, age?: number): string { if (age) { return `${name} --- ${age}`; } else { return `${name} ---年齡保密`; } } console.log(getInfo('zhangsan')); // zhangsan ---年齡保密 console.log(getInfo('zhangsan', 123)); // zhangsan --- 123
注意:可選參數必須配置到參數的最后面。
3、默認參數
es5 里面沒法設置默認參數,es6 和 ts 中都可以設置默認參數 。位置可以放在第一個參數,也可以放在最后一個參數的位置。
如果沒有傳年齡的參數,則默認為number=20,如果傳參數,就是傳的那個年齡的參數,和上面的可選參數是類似的。
function getInfo(name: string, age: number = 20): string { if (age) { return `${name} --- ${age}`; } else { return `${name} ---年齡保密`; } } console.log( getInfo('張三')); // 張三 --- 20 console.log(getInfo('張三', 30)); // 張三 --- 30
4、剩余參數
必要參數,默認參數和可選參數有個共同點:它們表示某一個參數。 有時,你想同時操作多個參數,或者你並不知道會有多少參數傳遞進來。 在 JavaScript 里,你可以使用 arguments 來訪問所有傳入的參數。在 TypeScript 里,你可以把所有參數收集到一個變量里。
剩余參數會被當做個數不限的可選參數。可以一個都沒有,同樣也可以有任意個。
例子:求和
function sum(a:number,b:number,c:number,d:number){ return a+b+c+d } sum (1,2,3,4); console.log(sum (1,2,3,4,)); // 10
例子:求積
function sum(a:number,b:number,c:number,d:number){ return a*b*c*d } sum (1,2,3,4); console.log(sum (1,2,3,4,)); // 24
三點運算符,接收形參傳過來的值(剩余參數)(把sum里面所有傳過來的參數全部賦值給result數組)
function sum(...result: number[]): number { var sum = 0; for (var i = 0; i < result.length; i++) { sum += result[i]; } return sum; } console.log(sum(1, 2, 3, 4, 5, 6)); // 21
另一種寫法,把1賦給a,2賦給b,后面三個數賦給...result
function sum(a: number, b: number, ...result: number[]): number { var sum = a + b; for (var i = 0; i < result.length; i++) { sum += result[i]; } return sum; } console.log(sum(1, 2, 3, 4, 5, 6)); // 21
5、函數重載
java中方法的重載:重載指的是兩個或者兩個以上同名函數,但它們的參數不一樣,這時會出現函數重載的情況
TS中的重載:通過為同一個函數提供多個函數類型定義來試下多種功能的目的
es5中出現同名方法,下面的會替換上面的方法
// 重載就相當於同名的函數,當es5中出現同名方法,下面的會替換上面的方法 function css(config){ } function css(config,value){ }
ts重載,通過為同一個函數提供多個函數類型定義來實現多種功能的目的
function getInfo(name: string): string; function getInfo(age: number): number; function getInfo(str: any): any{ if(typeof str === 'string'){ return '我叫:'+ str; }else{ return '我年齡:' + str; } } console.log(getInfo('張三')); // 我叫:張三 console.log(getInfo(20)); // 我年齡:20
TypeScript的重載,分別返回了字符串(張三)和number類型(20)
改下代碼:年齡屬性變為可選
function getInfo(name: string): string; function getInfo(name: string, age: number): string; function getInfo(name: any, age?: any): any { if (age) { return '我叫:' + name + ',我的年齡是' + age; } else { return '我叫:' + name; } } console.log(getInfo('張三')); // 我叫:張三 console.log(getInfo('張三', 20)); // 我叫:張三,我的年齡是20
再改下:年齡屬性給個默認值
function getInfo(name: string): string; function getInfo(name: string, age: number): string; function getInfo(name: any, age: number = 18): any { if (age) { return '我叫:' + name + ',我的年齡是' + age; } else { return '我叫:' + name; } } console.log(getInfo('張三')); // 我叫:張三,我的年齡是18 console.log(getInfo('張三', 20)); // 我叫:張三,我的年齡是20
TypeScript中的類
1、ts中類的定義
定義類的關鍵字為 class,后面緊跟類名,類可以包含以下幾個模塊(類的數據成員):
-
字段 − 字段是類里面聲明的變量。字段表示對象的有關數據。
-
構造函數 − 類實例化時調用,可以為類的對象分配內存。
-
方法 − 方法為對象要執行的操作。
創建類的數據成員:
以下實例我們聲明了類 Car,包含字段為 engine,構造函數在類實例化后初始化字段 engine。
this 關鍵字表示當前類實例化的對象。注意構造函數的參數名與字段名相同,this.engine 表示類的字段。
此外我們也在類中定義了一個方法 disp()。
class Car { // 字段 engine:string; // 構造函數 constructor(engine:string) { this.engine = engine } // 方法 disp():void { console.log("發動機為 : "+this.engine) } }
編譯上面代碼可以得到:
class Car { // 構造函數 constructor(engine) { this.engine = engine; } // 方法 disp() { console.log("發動機為 : " + this.engine); } }
創建實例化對象:
使用 new 關鍵字來實例化類的對象,語法格式如下:
var object_name = new class_name([ arguments ])
例子:創建來一個 Car 類,然后通過關鍵字 new 來創建一個對象並訪問屬性和方法:
class Car { // 字段 engine:string; // 構造函數 constructor(engine:string) { this.engine = engine } // 方法 disp():void { console.log("函數中顯示發動機型號 : " + this.engine) } } // 創建一個對象 var obj = new Car("XXSY1") // 訪問字段 console.log("讀取發動機型號 : " + obj.engine); // 讀取發動機型號 : XXSY1 // 訪問方法 obj.disp(); // 函數中顯示發動機型號 : XXSY1
例子1:
class Person { name: string; //屬性 前面省略了public關鍵詞 constructor(n: string) { //構造函數 實例化類的時候觸發的方法 this.name = n; } run(): void { console.log(this.name); } } var p = new Person('張三'); p.run(); // 張三
例子2:
class Person { name: string; constructor(name: string) { //構造函數 實例化類的時候觸發的方法 this.name = name; } getName(): string { return this.name; } setName(name: string): void { this.name = name; } } var p = new Person('張三'); console.log(p.getName()); // 張三 p.setName('李四'); console.log(p.getName()); // 李四
2、ts 中實現繼承 extends、super
類繼承使用關鍵字 extends,子類除了不能繼承父類的私有成員(方法和屬性)和構造函數,其他的都可以繼承。
TypeScript 一次只能繼承一個類,不支持繼承多個類,但 TypeScript 支持多重繼承(A 繼承 B,B 繼承 C)。
class Person { name: string; constructor(name: string) { this.name = name; } run(): string { return `${this.name}在運動` } } var p = new Person('王五'); console.log(p.run()); // 王五在運動 class Web extends Person { // 繼承 constructor(name: string) { super(name); /*初始化父類的構造函數*/ } work() { console.log(`${this.name}在工作`) } } var w = new Web('李四'); console.log(w.run()); // 李四在運動 console.log(w.work()); // 李四在工作
類繼承后,子類可以對父類的方法重新定義,這個過程稱之為方法的重寫。
其中 super 關鍵字是對父類的直接引用,該關鍵字可以引用父類的屬性和方法。
需要注意的是子類只能繼承一個父類,TypeScript 不支持繼承多個類,但支持多重繼承,如下實例:
class Person { name: string; constructor(name: string) { this.name = name; } run(): string { return `${this.name}在運動` } } class Child extends Person {} class Web extends Child {} // 多重繼承,繼承了 Child 和 Person 類 var w = new Web('張三'); console.log(w.run()); // 張三在運動
3、類里面的修飾符
TypeScript 中,可以使用訪問控制符來保護對類、變量、方法和構造方法的訪問。TypeScript 支持 3 種不同的訪問權限。屬性如果不加修飾符 默認就是 公有 (public)
-
public(默認) : 公有,可以在任何地方被訪問。
-
protected : 受保護,可以被其自身以及其子類和父類訪問。
-
private : 私有,只能被其定義所在的類訪問。
public : 公有,在當前類里面、子類 、類外面都可以訪問
在子類中:
class Person { public name: string; /*公有屬性*/ constructor(name: string) { this.name = name; } run(): string { return `${this.name}在運動` } } class Web extends Person { constructor(name: string) { super(name); /*初始化父類的構造函數*/ } run(): string { return `${this.name}在運動-子類` } work() { console.log(`${this.name}在工作`) } } var w = new Web('李四'); console.log(w.run()); // 李四在運動-子類 w.work(); // 李四在工作
類外部訪問公有屬性:
class Person { public name: string; /*公有屬性*/ constructor(name: string) { this.name = name; } run(): string { return `${this.name}在運動` } } var p = new Person('哈哈哈'); console.log(p.name); // 哈哈哈
protected:保護類型,在類里面、子類里面可以訪問,在類外部沒法訪問
在子類中:
class Person { protected name: string; /*保護屬性*/ constructor(name: string) { this.name = name; } run(): string { return `${this.name}在運動` } } class Web extends Person { constructor(name: string) { super(name); /*初始化父類的構造函數*/ } work() { console.log(`${this.name}在工作`) } } var w = new Web('李四'); w.work(); // 李四在工作 console.log(w.run()); // 李四在運動
類外外部沒法訪問保護類型的屬性:會報錯
class Person { protected name: string; /*保護類型*/ constructor(name: string) { this.name = name; } run(): string { return `${this.name}在運動` } } var p = new Person('哈哈哈'); console.log(p.name); // 會報錯
private :私有,在類里面可以訪問,子類、類外部都沒法訪問
在子類中:會報錯
class Person { private name: string; /*私有*/ constructor(name: string) { this.name = name; } run(): string { return `${this.name}在運動` } }
class Web extends Person { constructor(name: string) { super(name) } work() { console.log(`${this.name}在工作`) } }
在類外部里:會報錯
class Person { private name: string; /*私有*/ constructor(name: string) { this.name = name; } run(): string { return `${this.name}在運動` } } var p = new Person('哈哈哈'); console.log(p.name); console.log(p.run());
4、靜態屬性 靜態方法
static 關鍵字用於定義類的數據成員(屬性和方法)為靜態的,靜態成員可以直接通過類名調用。
例子1:
class Father { static myName: string = 'mySkey'; static getAge(): number{ return 23; } } console.log(Father.myName); // mySkey console.log(Father.getAge()); // 23
例子2:
class Per { public name: string; public age: number = 20; //靜態屬性 static sex = "男"; constructor(name: string) { this.name = name; } run() { /*實例方法*/ console.log(`${this.name}在運動`) } work() { console.log(`${this.name}在工作`) } static print() { /*靜態方法 里面沒法直接調用類里面的屬性*/ console.log('print方法' + Per.sex); } } Per.print(); // print方法男 console.log(Per.sex); // 男
5、只讀
readonly 申明的只讀的屬性不能再更改
class Person { readonly name: string = 'mySkey'; } let mySkey = new Person(); console.log(mySkey.name); // mySkey mySkey.name = '1111'; // Error 報錯
6、get 與 set 來截取對對象成員的訪問
TypeScript支持getters/setters來截取對對象成員的訪問。 它能幫助你有效的控制對對象成員的訪問
// 就像女人的年齡是個秘密,只會對不喜歡的人告訴真實的 let isLike: boolean = false; class Woman{ private _age: number = 16; get age(): number{ return this._age; } set age(num: number){ if(!isLike){ this._age = num; }else{ console.log('告訴你的也是假的!!') } } } let woman = new Woman(); woman.age = 23; console.log(woman.age); // 23 // 看吧,不喜歡你,所以你知道了她的真實年齡,如果把isLike改為true,那么她每天就是16了 let isLike: boolean = true; class Woman{ private _age: number = 16; get age(): number{ return this._age; } set age(num: number){ if(!isLike){ this._age = num; }else{ console.log('告訴你的也是假的!!') } } } let woman = new Woman(); woman.age = 23; console.log(woman.age); // 告訴你的也是假的!! // 16
7、多態
父類定義一個方法不去實現,讓繼承它的子類去實現 每一個子類有不同的表現
class Animal { name: string; constructor(name: string) { this.name = name; } eat() { //具體吃什么 不知道 , 具體吃什么?繼承它的子類去實現 ,每一個子類的表現不一樣 console.log('吃的方法') } } class Dog extends Animal { constructor(name: string) { super(name) } eat() { console.log(this.name + '啃骨頭') } } class Cat extends Animal { constructor(name: string) { super(name) } eat() { console.log(this.name + '吃魚') } } let ANAME: string = "Tom"; var D = new Dog(ANAME); D.eat(); // Tom啃骨頭 var C = new Cat(ANAME); C.eat(); // Tom吃魚
8、抽象類
typescript 中的抽象類:它是提供其他類繼承的基類,不能直接被實例化。
用 abstract 關鍵字定義抽象類和抽象方法,抽象類中的抽象方法不包含具體實現並且必須在派生類中實現
abstract 抽象方法只能放在抽象類里面
例子1:
abstract class Person { abstract type: string = 'animal'; // 抽象類 sing(){ console.log('我會唱歌!') } } // let person = new Person() // Error 無法創建抽象類的實例 class Myskey extends Person { type: string = 'animal123'; // 無法繼承,而且必須重寫 constructor(){ super() this.sing() } } var my = new Myskey(); // 我會唱歌! console.log(my.type); // animal123
例子2:
abstract class Animal { public name: string; constructor(name: string) { this.name = name; } abstract eat(): any; //抽象方法不包含具體實現並且必須在派生類中實現。 run() { console.log('其他方法可以不實現') } } // var a = new Animal() /*錯誤的寫法*/ class Dog extends Animal { //抽象類的子類必須實現抽象類里面的抽象方法 constructor(name: any) { super(name) } eat() { console.log(this.name + '吃糧食') } } class Cat extends Animal { //抽象類的子類必須實現抽象類里面的抽象方法 constructor(name: any) { super(name) } eat() { console.log(this.name + '吃老鼠') } } var d = new Dog('小花花'); d.eat(); // 小花花吃糧食 var c = new Cat('小花貓'); c.eat(); // 小花花吃老鼠
TypeScript中的接口
接口的作用:在面向對象的編程中,接口是一種規范的定義,它定義了行為和動作的規范,在程序設計里面,接口起到一種限制和規范的作用。
接口定義了某一批類所需要遵守的規范,接口不關心這些類的內部狀態數據,也不關心這些類里方法的實現細節,它只規定這批類里必須提供某些方法,提供這些方法的類就可以滿足實際需要。
typescrip 中的接口類似於java,同時還增加了更靈活的接口類型,包括屬性、函數、可索引和類等
1、ts 中自定義方法傳入參數,對 json 進行約束
function printLabel(labelInfo: { label: string }): void { console.log('printLabel', labelInfo); } // printLabel('張三'); //錯誤寫法 // printLabel({name:'張三'}); //錯誤的寫法 printLabel({label:'張三'}); //正確的寫法 printLabel { label: '張三' }
2、接口 interface:行為和動作的規范,對批量方法進行約束
//就是傳入對象的約束 屬性接口 interface FullName { firstName: string; //注意;結束 secondName: string; } function printName(name: FullName) { // 必須傳入對象 firstName secondName console.log(name.firstName + '--' + name.secondName); } // printName('1213'); // 錯誤 var obj = { /*傳入的參數必須包含 firstName secondName*/ age: 20, firstName: '張', secondName: '三' }; printName(obj); // 張--三
修改為:
//就是傳入對象的約束 屬性接口 interface FullName { firstName: string; //注意;結束 secondName: string; age: number; } function printName(name: FullName) { // 必須傳入對象 firstName secondName console.log(name.firstName + '--' + name.secondName + '--' + name.age ); } // printName('1213'); // 錯誤 var obj = { /*傳入的參數必須包含 firstName secondName*/ age: 20, firstName: '張', secondName: '三' }; printName(obj); // 張--三--20
3、參數的順序可以不一樣
interface FullName { firstName: string; secondName: string; } function getName(name: FullName) { console.log(name) } // 參數的順序可以不一樣 getName({ secondName: 'secondName', firstName: 'firstName' }) // { secondName: 'secondName', firstName: 'firstName' }
4、也可以設置可選屬性
interface FullName { firstName: string; secondName?: string; } function getName(name: FullName) { console.log(name) } getName({ firstName: 'firstName' }) // { firstName: 'firstName' }
注意:屬性接口中定義的屬性,在傳入的參數必須全部包含,否則會報錯。或者可以設置為可選屬性。
例如:ajax
interface Config { type: string; url: string; data?: string; dataType: string; } //原生js封裝的ajax function ajax(config: Config) { var xhr = new XMLHttpRequest(); xhr.open(config.type, config.url, true); xhr.send(config.data); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { console.log('success!!!'); if (config.dataType == 'json') { console.log(JSON.parse(xhr.responseText)); } else { console.log(xhr.responseText) } } } } ajax({ type: 'get', data: 'name=zhangsan', url: 'http://a.itying.com/api/productlist', //api dataType: 'json' })
TypeScript中的泛型
軟件工程中,我們不僅要創建一致的定義良好的 API,同時也要考慮可重用性。 組件不僅能夠支持當前的數據類型,同時也能支持未來的數據類型,這在創建大型系統時為你提供了十分靈活的功能。
在像 C# 和 Java 這樣的語言中,可以使用泛型來創建可重用的組件,一個組件可以支持多種類型的數據。 這樣用戶就可以以自己的數據類型來使用組件。
function add<T>(a: T, b: T): void { console.log(a) console.log(b) } add(1, 2) // 1 // 2 add('a', 'b') // a // b
接口 + 泛型 -> 函數類型
let addFn: AddFn<boolean> = function (a, b) { console.log(a) console.log(b) return [{ id: 1, name: '浙大正呈' }] } interface arrIF { readonly id: number, name: string } interface AddFn<T> { (a: T, b: number): arrIF[] } console.log(addFn(false, 2)) // false // 2 // [ { id: 1, name: '浙大正呈' } ]
TypeScript熟悉使用者?
- 查看最新TypeScript版本的新特性
- 深入學習研究TypeScript手冊指南
- 閱讀.d.ts創建指南
https://www.jianshu.com/p/0ebd56cb22d2
https://www.bilibili.com/video/BV1Qa4y1E7U1/?spm_id_from=333.788.videocard.15