聲明 本篇內容摘抄自以下來源: "TypeScript 中文網" 只梳理其中部分知識點,更多更詳細內容參考官網。 正文 TypeScript 今天來講講有 Java 基礎轉 JavaScript 的福音:TypeScript 為什麼學習 TypeScript 如果學習 JavaScript 之前已經 ...
聲明
本篇內容摘抄自以下來源:
只梳理其中部分知識點,更多更詳細內容參考官網。
正文-TypeScript
今天來講講有 Java 基礎轉 JavaScript 的福音:TypeScript
為什麼學習 TypeScript
如果學習 JavaScript 之前已經有了 Java 的基礎,那麼學習過程中肯定會有很多不習慣的地方,因為 JavaScript 不管是在語法上面、還是編程思想上與 Java 這類語言都有一些差異。
下麵就大概來看幾個方面的差異:
變數聲明
JavaScript 是弱語言,聲明變數時無需指明變數的數據類型,運行期間會自動推斷,所以聲明方式很簡單:
var a = 1;
var wx = "dasu_Android"
Java 是強類型語言,聲明變數時必須明確指出變數數據類型:
int a = 1;
String wx = "dasu_Android";
弱類型語言雖然比較靈活,但也很容易出問題,而且需要一些額外的處理工作,比如函數期待接收數組類型的參數,但調用時卻傳入了字元串類型,此時 js 引擎並不會報錯,對於它來說,這是合理的行為,但從程式、從功能角度來看,也許就不會按照預期的執行,所以通常需要在函數內部進行一些額外處理,如果沒有額外處理,那麼由於這種參數類型導致的問題也很難排查。
變數作用域
JavaScript 的變數在 ES5 只有全局作用域和函數內作用域,ES6 新增了塊級作用域。
Java 的變數分:類變數和實例變數,屬於類的變數如果是公開許可權,那麼所有地方都允許訪問,屬於實例的變數,分成員變數和局部變數,成員變數在實例內部所有地方都可以訪問,在實例外部如果是公開許可權,可通過對象來訪問,局部變數只具有塊級作用域。
- 變數被覆蓋問題
因為 JavaScript 在 ES5 時並沒有塊級作用域,有些場景下會導致變數被覆蓋的情況,由於這種情況造成的問題也很難排查,比如:
function aaa() {
var i = -1;
for (var i = 0; i < 1; i++) {
for (var i = 1; i < 2; i++) {
}
console.log(i);
}
console.log(i);
}
在 Java 中,兩次 i 的輸出應該 0, -1,因為三個地方的 i 變數並不是同一個,塊級作用域內又生成一個新的局部 i 變數,但在 JavaScript 里,ES5 沒有塊級作用域,函數內三個 i 都是同一個變數,程式輸出的是:2,3。此時就發送變數被覆蓋的情況了,。
- 拼寫錯誤問題
而且,JavaScript 的全局變數會被作為全局對象的屬性存在,而在 JavaScript 里對象的屬性是允許動態添加的,這就會導致一個問題:當使用某變數,但拼寫錯誤時,js 引擎並不會報錯,對它來說,會認為新增了一個全局對象的屬性;但從程式,從功能角度來看,常常就會導致預期外的行為,而這類問題也很難排查,比如:
var main = "type-script";
function modify(pre) {
mian = `${pre}-script`;
}
modify(123);
在 Java 里會找不到 mian 變數報錯,但在 JavaScript 里 mian 會被當做全局對象的屬性來處理。
- 全局變數衝突問題
而且,JavaScript 的變數允許重覆申請,這樣一來,全局變數一旦多了,很容易造成變數衝突問題,這類問題即使在運行期間也很難被髮現和排查,比如:
//a.js
var a = 1;
//b.js
var a = "js";
在不同文件中,如果全局變數命名一樣,會導致變數衝突,但瀏覽器不會有任何報錯行為,因為對它來說,這是正常的行為,但對於程式來說,功能可能就會出現預期外的行為。
繼承
JavaScript 是基於原型的繼承,原型本質上也是對象,所以 JavaScript 中對象是從對象上繼承的,同時對象也是由對象創建的,一切都是對象。
Java 中有 class 機制,對象的抽象模板概念,用於描述對象的屬性和行為以及繼承結構,而對象是從類實例化創建出來的。
正是因為 JavaScript 中並沒有 class 機制,所以有 Java 基礎的可能會比較難理解 JavaScript 中的繼承、實例化對象等原理。
那麼在面向對象的編程中,自定義了某個對象,並賦予它一定的屬性和行為,這樣的描述在 Java 里很容易實現,但在 JavaScript 里卻需要通過定義構造函數,對構造函數的 prototype 操作等處理,語義不明確,不怎麼好理解,比如定義 Dog 對象:
function Dog() {}
Book.prototype.eat = function () {
//...
}
Book.prototype.name = "dog";
對於習慣了 Java 的面向對象編程,在 JavaScript 里自定義一個 Dog 對象的寫法可能會很不習慣。
Class 機制
JavaScript 雖然在 ES6 中加入了 class 寫法,但本質上只是語法糖,而且從使用上,仍舊與 Java 的 class 機制有些區別,比如:
class Animal {
constructor(theName) {
this.name = theName;
this.ll = 23;
}
move(distanceInMeters = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
以上是 JavaScript 中 ES6 自定義某個類的用法,與 Java 的寫法有如下區別:
- 類的屬性只能在構造函數內聲明和初始化,無法像 Java 一樣在構造函數外面先聲明成員變數的存在;
- 無法定義靜態變數或靜態方法,即沒有 static 語法;
許可權控制
JavaScript 里沒有 public 這些許可權修飾符,對於對象的屬性,只能通過控制它的可配置性、可寫性、可枚舉性來達到一些限制效果,對於對象,可通過控制對象的可擴展性來限制。
Java 里有 package 許可權、publick 許可權、protection 許可權、private 許可權之分,許可權修飾符可修飾類、變數、方法,不同許可權修飾符可以讓被修飾的具有不一樣的許可權限制。
在 JavaScript 如果要實現對外部隱藏內部實現細節,大多時候,只能利用閉包來實現。
抽象類
JavaScript 雖然在 ES6 中引入了 class 的寫法,但本質上只是語法糖,並沒有類似 Java 中抽象類、抽象方法的機制存在,即使要模擬,也只能是定義一些拋異常的方法來模擬抽象方法,子類不實現的話,那麼在運行期間就會拋異常,比如:
//不允許使用該構造函數創建對象,來模擬抽象類
function AbstractClass() {
throw new Error("u can't instantiate abstract class");
}
//沒有實現的抽象方法,通過拋異常來模擬
function abstractMethod() {
throw new Error("abstract method,u should implement it");
}
//定義抽象方法,子類繼承之後,如果不自己實現,直接使用會拋異常
AbstractClass.prototype.onMearsure = abstractMethod;
AbstractClass.prototype.onLayout = abstractMethod;
相比於 Java 的抽象類的機制,在編譯期間就可以報錯的行為,JavaScript 的運行期拋異常行為效果可能沒法強制讓所有開發者都能正確實現抽象方法。
對象標識
JavaScript 由於沒有 class 機制,又是基於原型的繼承,運行期間原型還可動態變化,導致了在 JavaScript 里沒有一種完美的方式可以用來獲取對象的標識,以達到區分不同對象的目的。
Java 中的對象都是從類實例化創建出來的,因此通過 instanceof 即可判斷不同對象所屬類別是否一致。
在 JavaScript 中,只能根據不同使用場景,選擇 typeof,instanceof,isPrototypeOf(),對象的類屬性,對象的構造函數名等方式來區別不同對象所屬類別。
鴨式辯型
正是由於 JavaScript 里沒有 class 機制,沒有哪種方式可以完美適用所有需要區分對象的場景,因此在 JavaScript 中有一種編程理念:鴨式辯型(只要會游泳且嘎嘎叫的鳥,也可以認為它是鴨子)
意思就是說,編程中不要從判斷對象是否是預期的類別角度出發,而是從判斷對象是否具有預期的屬性角度出發。
小結
所以,對於如果有 Java 基礎的,JavaScript 學習過程可能會有些不習慣,那麼如果是 TypeScript 的話,可以說是個福利,因為 TypeScript 很多語法和編程思想上都跟 Java 很類似,很容易就理解。
那麼,來認識下,TypeScript 是什麼?
TypeScript 是 JavaScript 的超集,超集是什麼意思,就是說,JavaScript 程式可以不加修改就運行在 TypeScript 的環境中,TypeScript 在語法上是基於 JavaScript 進行擴展的。
那麼,TypeScript 在 JavaScript 語法基礎上做了哪些擴展呢?其實就是加入了各種約束性的語法,比如加入了類似強類型語言的語法。
比如說,聲明變數時,需要指定變數的數據類型的約束,以此來減少類型錯誤導致的問題。
let wx:string = "dasu_Android";
其實,本質上是因為 JavaScript 是解釋型語言,因為沒有編譯階段,很多問題只能是運行期才可能被髮現,而運行期暴露的問題也不一定可以很好的排查出來。
而 TypeScript 語法編寫的 ts 文件代碼,瀏覽器並不認識,所以需要經過一個編譯階段,編譯成 js 文件,那麼 TypeScript 就提供了一個編譯過程,加上它語法上的支持,在編譯期間編譯器就可以幫助開發者找出一些可能出錯的地方。
舉個例子:
var main = "type-script";
function modify(pre) {
mian = `${pre}-script`;
}
modify(123);
這個例子中,定義了一個全局變數和一個函數,函數本意是接收一個字元串類型的值,然後修改這個全局變數的值,但開發者可能由於粗心,將全局變數的變數名拼寫錯誤了,而且調用方法時並沒有傳入字元串類型,而是數字類型。
如果是在 JavaScript 中,這段代碼運行期間並不會報錯,也不會導致程式異常,js 解釋器會認為它是合理的,它會認為這個函數是用來增加全局對象的 mian 屬性,同時函數參數它也不知道開發者希望使用的是什麼類型,它所有類型都接受。
由於程式並沒有出現異常,即使運行期間,開發者也很難發現這個拼寫錯誤的問題,相反,程式由於拼寫錯誤而沒有執行預期的功能時,反而會讓開發者花費很多時間來排查原因。
但這段代碼如果是用 TypeScript 來寫:
這些基礎的語法錯誤,編譯器甚至不用進入編譯階段,在開發者剛寫完這些代碼就能給出錯誤提示。而且,一些潛在的可能造成錯誤的代碼,在編譯階段也會給出錯誤提示。
雖然 TypeScript 語法上支持了很多類似於 Java 語言的特性,比如強類型約束等,但 JavaScript 本質上並不支持,可以看看上面那段代碼最後編譯成的 js 代碼:
var main = "type-script";
function modify(pre) {
mian = `${pre}-script`;
}
modify(123);
發現沒有,編譯後的代碼其實也就是上述舉例的 js 代碼段,也就是說,你用 JavaScript 寫和用 TypeScript 寫,最後的代碼都是一樣的,區別在於,TypeScript 它有一個編譯階段,藉助編譯器可以在編譯期就能發現可能的語法錯誤,不用等到運行期。
WebStrom 配置
將 TypeScript 編寫的 ts 文件編譯成 js 文件有兩種途徑,一是藉助命令,二是藉助開發工具。
如果電腦已經有安裝了 node.js 環境,那麼可以直接執行下述命令:
npm install -g typescript
然後打開終端,在命令行執行:
tsc xxx.ts
tsc 命令就可以將 ts 文件編譯輸出 js 文件了。
我選擇的開發工具是 WebStrom,這個開發工具本身就是支持 TypeScript 的了,如果你有嘗試過查看 ES5、ES6 相關 api,你可能會發現:
.d.ts 文件就是用 TypeScript 編寫的,所以如果你熟悉 TypeScript 的語法,這些代碼就能很清楚了,.d.ts 是一份聲明文件,作用類似於 C++ 中的 .h 文件。
在 WebStrom 中右鍵 -> 新建文件中,可以選擇創建 TypeScript 的文件,可以設置 FileWatcher 來自動編譯,也可以將項目初始化成 node.js 項目,利用 package.json 里的 scripts 腳本命令來手動觸發編譯。
我選擇的是後者,如果你對 package.json 或 FileWatcher 配置不熟悉,可以參考之前模塊化那篇最後對這些配置的介紹。
而編譯器在編譯過程,類似於 Android 里的 Gradle,可以設置很多配置項,進行不同的編譯,而 TypeScript 編譯過程對應的配置文件是 tsconfig.json
tsconfig.json
TypeScript 中文網 里對於這份配置文件的描述很清楚了,這裡摘抄部分內容:
- 不帶任何輸入文件的情況下調用 tsc,編譯器會從當前目錄開始去查找 tsconfig.json 文件,逐級向上搜索父目錄。
- 不帶任何輸入文件的情況下調用 tsc,且使用命令行參數 --project(或 -p)指定一個包含 tsconfig.json 文件的目錄。
- 當命令行上指定了輸入文件時,tsconfig.json 文件會被忽略。
示例:
{
"compilerOptions": {
"module": "commonjs", //編譯輸出的 js 以哪種模塊規範實現,有 commonjs,amd,umd,es2015等等
"target": "es5", //編譯輸出的 js 以哪種 js 標準實現,有 es3,es5,es6,es2015,es2016,es2017,es2018等
"sourceMap": false, //編譯時是否生成對應的 source map 文件
"removeComments": false, //編譯輸出的 js 文件刪掉註釋
"outDir": "./js/dist" //編譯輸出的 js 路徑
},
"exclude": [ //編譯時排除哪些文件
"node_modules"
]
}
語法
最後來看看一些基礎語法,你會發現,如果你有 Java 基礎,這些是多麼的熟悉,用 TypeScript 來編寫 js 代碼是多麼的輕鬆。
數據類型
ES6 中的數據類型是:number,string,boolean,symbol,null,undefined,object
TypeScript 在此基礎上,額外增加了:any,void,enum,never
- any:表示當前這個變數可以被賦予任何數據類型使用;
- void:表示當前這個變數只能被賦予 null 或 undefined,通常用於函數的返回值聲明;
- enum:枚舉類型,彌補 JavaScript 中無枚舉的數據類型;
- never:表示永不存在的值,常用於死迴圈函數,拋異常函數等的返回值聲明,因為這些函數永遠也不會有一個返回值。
TypeScript 中的數據類型是用於類型聲明服務的,類似於 Java 中定義變數或聲明方法的返回值時必須指定一個類型。
類型聲明
ES5 中聲明變數是通過 var,而 ES6 中引入塊級作用域後新增了 let 和 const 的聲明方式,TypeScript 建議聲明變數時,都使用 let,因為 var 會很容易造成很多問題,不管是全局變數還是函數作用域的局部變數。
先來看看原始類型的聲明:
let num:number = 1; //聲明number類型變數
let str:string = "ts"; //聲明string類型變數
let is:boolean = true; //聲明boolean類型變數
function f(name: string, age: number):void { //函數參數類型和返回值類型的聲明
//...
}
聲明一個變數時,就可以在變數名後面跟 :
冒號來聲明變數的數據類型,如果賦值給變數聲明的數據類型之外的類型,編譯器會有錯誤提示;函數的返回值的類型聲明方式類似。
如果某個變數的取值可以是任意類型的,那麼可以聲明為 any:
let variable:any = 1; //聲明可為任意類型的變數
variable = true;//此時賦值其他類型都不會報錯
如果某個變數取值只能是某幾個類型之間,可以用 |
聲明允許的多個類型:
let numStr:number|string = 1; //聲明可為string或number類型變數
numStr = "str";
numStr = true;// 報錯
如果變數是個數組:
let numArr:number[] = [1, 2]; //聲明純數字數組,如果某個元素不是數字類型,會報錯
let anyArr:any[] = [1, "tr", true]; //數組元素類型不限制
let numStrArr:(number|string)[] = [1, "tr", 2, 4]; // 數組元素類型限制在只能是 number 和 string
如果變數是個對象:
let obj:object = {};
但這通常沒有什麼意義,因為函數,數組,自定義對象都屬於 object,所以可以更具體點,比如聲明變數是個函數:
let fun:(a:number)=>string = function (a:number):string { //聲明函數類型的變數
return "";
}
聲明 fun 變數是一個函數類型時,還需要將函數的結構聲明出來,也就是函數參數,參數類型,返回值類型,通過 ES6 的箭頭函數語法來聲明。
但賦值的時候,賦值的函數參數類型,返回值類型可以不顯示聲明,因為編譯器可以根據函數體來自動推斷,比如:
let fun:(a:number)=>string = function (a) {
return "";
}
如果變數是某個自定義的對象:
class Dog {
name:string;
age:number = 0;
}
let dog:Dog = new Dog(); //聲明自定義對象類型的變數
定義類的語法後面介紹,在 JavaScript 里,鴨式辯型的編程理念比較適用,也就說,判斷某個對象是否歸屬於某個類時,並不是看這個對象是否是從這個類創建出來的,而是看這個對象是否具有類的特征,即類中聲明的屬性,對象是否擁有,有,則認為這個對象是屬於這個類的。如:
let dog:Dog = {name:"dog", age:123}; //可以賦值成功,因為對象直接量具有 Dog 類中的屬性
let dog1:Dog = {name:"dog", age:1, sex:"male"}; //錯誤,多了個 sex
let dog2:Dog = {name:"dog"}; //錯誤,少了個 age
let dog3:Dog = {name:"dog", age:"12"}; //錯誤,age 類型不一樣
以上例子中:
let dog1:Dog = {name:"dog", age:1, sex:"male"};
從鴨式辯型角度來說,這個應該是要可以賦值成功的,因為目標對象擁有類指定的特征行為了,TypeScript 覺得額外多出的屬性可能會造成問題,所以會給一個錯誤提示。
針對這種因為額外多出的屬性檢查而報錯的情況,如果想要繞開這個限制,有幾種方法:
- 類型斷言
let dog1:Dog = <Dog>{name:"dog", age:1, sex:"male"};
let dog1:Dog = {name:"dog", age:1, sex:"male"} as Dog;
類型斷言就是類似 Java 里的強制類型轉換概念,通過 <>
尖括弧或者 as
關鍵字,可以告訴編譯器這個值的數據類型。
類型斷言常用於開發者明確知道某個變數的數據類型的情況下。
- 用變數做中轉賦值
如果賦值語句右側是一個變數,而不是對象直接量的話,那麼只會檢查變數是否擁有賦值語句左側所聲明的類型的特征,而不會去檢查變數額外多出來的屬性,如:
let o = {name:"dog", age:1, sex:"male"};
let dog1:Dog = o;
- 剩餘屬性
這種方式是最佳的方式,官網中對它的描述是字元串索引簽名,但我覺得這個描述很難理解,而且看它實現的方式,有些類似於 ES6 中的函數的剩餘參數的處理,所以我乾脆自己給它描述成剩餘屬性的說法了。
方式是這樣的,在類中定義一個用於存儲其他沒有聲明的屬性數組:
class Dog {
name:string;
age:number = 0;
[propName:string]:any;
}
最後一行 [propName:string]:any
就表示:具有 Dog 特征的對象除了需要包含 name 和 age 屬性外,還可以擁有其他任何類型的屬性。所以:
let dog1:Dog = {name:"dog", age:1, sex:"male", s:true};
這樣就是被允許的了。
當然,這三種可以繞開多餘屬性的檢查手段,應該適場景而使用,不能濫用,因為,大部分情況下,當 TypeScript 檢查出你賦值的對象多了某個額外屬性時,程式會因此而出問題的概念是比較大的。
鴨式辯型在 TypeScript 里更常用的是利用介面來實現,後續介紹。
介面
鴨式辯型其實嚴格點來講就是對具有結構的值進行類型檢查,而具有結構的值也就是對象了,所以對對象的類型檢查,其實也就是在對對象進行類別劃分。
既然是類別劃分,那麼不同類別當然需要有個標識來相互區分,在 TypeScript 里,介面的作用之一也就是這個,作為不同對象類別劃分的依據。
比如:
interface Dog {
name:string;
age:number;
eat():any;
}
上述就是定義了,對象如果擁有 name, age 屬性和 eat 行為,那麼就可以將這個對象歸類為 Dog,即使創建這個對象並沒有從實現了 Dog 介面的類上實例化,如:
let dog:Dog = {
name: "小黑",
age:1,
eat: function () {
//...
}
}
上述代碼聲明瞭一個 Dog 類型的變數,那麼什麼對象才算是 Dog 類型,只要擁有 Dog 中聲明的屬性和行為就認為這個對象是 Dog,這就是鴨式辯型。(屬性和行為是 Java 裡面向對象常說的概念,屬性對應變數,行為對應方法,在 JavaScript 里變數和方法都屬於對象的屬性,但既然 TypeScript 也有類似 Java 的介面和類語法,所以這裡我習慣以 Java 那邊的說法來描述了,反正能理解就行)
當然,也可以通過定義一個 Dog 類來作為變數的類型聲明,但介面相比於類的好處在於,介面里只能有定義,一個介面里具有哪些屬性和行為一目瞭然,而類中常常攜帶各種邏輯。
既然介面作用之一是用來定義對象的類別特征,那麼,它還有很多其他的用法,比如:
interface Dog {
name:string;
age:number;
eat:()=>any;
master?:string; //狗的主人屬性,可有可無
readonly variety:string; //狗的品種,一生下來就固定了
}
let dog1:Dog = {name:"dog1", age:1, eat:()=>"", variety:"柯基"};
dog1.age = 2;
dog1.variety = "中華犬";//報錯,variety聲明時就被固定了,無法更改
let dog2:Dog = {name:"dog2", age:1, eat:()=>"", master: "me",variety:"柯基"};
在介面里聲明屬性時,可用 ?
問號表示該屬性可有也可沒有,可用 readonly 來表示該屬性為只讀屬性,那麼在定義時初始化後就不能再被賦值。
?
問號用來聲明該項可有可無不僅可以用於在定義介面的屬性時使用,還可以用於聲明函數參數時使用。
在類型聲明一節中說過,聲明一個變數的類型時,也可以聲明為函數類型,而函數本質上也是對象,所以,如果有需求是需要區分多個不同的函數是否屬於同一個類別的函數時,也可以用介面來實現,如:
interface Func {
(name:string):boolean;
}
let func:Func = function (name) {
return true;
}
這種使用介面的方式稱為聲明函數類型的介面,可以簡單的理解為,為 Func 類型的變數定義了 ()
運算符,需傳入指定類型參數和返回指定類型的值。
如果想讓某個類型既可以當做函數被調用,又可以作為對象,擁有某些屬性行為,那麼可以結合上述聲明函數類型的介面方式和正常的介面定義屬性行為方式一起使用。
當對象或函數作為函數參數時,通過介面來定義這些參數的類型,就特別有用,這樣可以控制函數調用時傳入了預期類型的數據,如果類型不一致時,編譯階段就會報錯。
當然,介面除了用來在鴨式辯型中作為值類型的區分外,也可以像 Java 里的介面一樣,定義一些行為規範,強制實現該介面的類的行為,如:
interface Dog {
name:string;
age:number;
eat:()=>any;
master?:string; //狗的主人屬性,可有可無
readonly variety:string; //狗的品種,一生下來就固定了
}
class ChinaDog implements Dog{
age: number = 0;
eat: () => any;
master: string;
name: string;
readonly variety: string = "中華犬";
}
ChinaDog 實現了 Dog 介面,那麼就必須實現該介面所定義的屬性行為,所以,ChinaDog 創建的對象明顯就屬於 Dog:
let dog3:Dog = new ChinaDog();
除了這些基本用法外,TypeScript 的介面還有其他很多用法,比如,定義構造函數:
interface Dog {
new (name:string): Dog;
}
再比如介面的繼承:介面可繼承自介面,也可繼承自類,繼承的時候,可同時繼承多個等。
更多高級用法,等有具體的使用場景,碰到的時候再深入去學習,先瞭解到這程度吧。
Class 語法
習慣 Java 代碼後,首次接觸 ES5 多多少少會很不適應,因為 ES5 中都是基於原型的繼承,沒有 class 概念,自定義個對象都是寫構造函數,寫 prototype。
後來 ES6 中新增了 class 語法糖,可以類似 Java 一樣通過 class 自定義對象,但還是有很多區別,比如,ES6 中的 class 語法糖,就無法聲明成員變數,成員變數只能在構造函數內定義和初始化;而且,也沒有許可權控制、也沒有抽象方法機制、也不能定義靜態變數等等。
然而,這一切問題,在 TypeScript 中都得到瞭解決,TypeScript 的 class 語法基本跟 Java 一樣,有 Java 基礎的,學習 TypeScript 的 class 語法會很輕鬆。
看個例子:
abstract class Animal { //定義抽象類
age:number;
protected abstract eat():void; //抽象方法,許可權修飾符
}
class Dog extends Animal{ //類的繼承
public static readonly TAG:string = "Dog"; //定義靜態常量
public name:string;
private isDog:boolean = true; //定義私有變數
constructor(name:string) {
super();
this.name = name;
this.age = 0;
}
protected eat:()=>any = function () {
this.isDog;
}
get age():number { //將 age 定義為存取器屬性
return this.age;
}
set age(age:number) { //將 age 定義為存取器屬性
if (age > 0) {
this.age = age;
} else {
age = 0;
}
}
}
let dog:Dog = new Dog("小黑");
大概有幾個地方跟 Java 有些許小差別:
- 變數類型的聲明
- 構造函數不是用類名錶示,而是使用 constructor
- 如果有繼承關係,則構造函數中必須要調用super
- 不手動使用許可權修飾符,預設是 public 許可權
其餘方面,不管是許可權的控制、繼承的寫法、成員變數的定義或初始化、抽象類的定義、基本上都跟 Java 的語法差不多。
所以說 TypeScript 的 class 語法比 ES6 的 class 語法糖要更強大。
還有很多細節的方面,比如在構造函數的參數前面加上許可權修飾符,此時這個參數就會被當做成員變數來處理,可以節省掉賦值的操作;
比如在 TypeScript 里,類還可以當做介面來使用。更多用法後續有深入再慢慢記錄。
泛型
Java 里在操作實體數據時,經常會需要用到泛型,但 JavaScript 本身並不支持泛型,不過 TypeScript 支持,比如:
interface Adapter<T> {
data:T;
}
class StringAdapter implements Adapter<string>{
data: string;
}
function f1<Y extends Animal>(arg:Y):Y {
return;
}
f1(new Dog("小黑"));
Dog 和 Animal 使用的是上個小節中的代碼。
用法基本跟 Java 類似,函數泛型、類泛型、泛型約束等。
模塊
JavaScript 跟 Java 很不一樣的一點就是,Java 有 class 機制,不同文件都需要有一個 public class,每個文件只是用於描述一個類的屬性和行為,類中的變數不會影響其他文件內的變數,即使有同名類,只要類文件路徑不一致即可。
但 JavaScript 所有的 js 文件都是運行在全局空間內,因此如果不在函數內定義的變數都屬於全局變數,即使分散在多份不同文件內,這就很容易造成變數衝突。
所以也才有那麼多模塊化規範的技術。
雖然 TypeScript 的 class 語法很類似於 Java,但 TypeScript 最終仍舊是要轉換成 JavaScript 語言的,因此即使用 TypeScript 來寫 class,只要有出現同名類,那麼即使在不同文件內,仍舊會造成變數衝突。
解決這個問題的話,TypeScript 也支持了模塊化的語法。
而且,TypeScript 模塊化語法有一個好處是,你只需掌握 TypeScript 的模塊化語法即可,編譯階段可以根據配置轉換成 commonJs, amd, cmd, es6 等不同模塊化規範的實現。
TypeScript 的語法跟 ES6 中的模塊語法很類似,只要 ts 文件內出現 import 或 export,該文件就會被當做模塊文件來處理,即整個文件內的代碼都運行在模塊作用域內,而不是全局空間內。
- 使用 export 暴露當前模塊對外介面
//module.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
class AarCodeValidator implements StringValidator {
isAcceptable(s: string) {
//...
}
}
export { AarCodeValidator };
export 的語法基本跟 ES6 中 export 的用法一樣。
如果其他模塊需要使用該模塊的相關介面:
- 使用 import 依賴其他模塊的介面
import { ZipCodeValidator } from "./module";
let myValidator = new ZipCodeValidator();
如果想描述非 TypeScript 編寫的類庫的類型,我們需要聲明類庫所暴露出的API。通常需要編寫 .d.ts 聲明文件,類似於 C++ 中的 .h 文件。
.d.ts 聲明文件的編寫,以及引用時需要用到三斜杠指令:
/// <reference path="./m2.d.ts"/>
這部分內容我還沒理解清楚,後續碰到實際使用掌握後再來說說。
命名空間
命名空間與模塊的區別在於,模塊會涉及到 import 或 export,而命名空間純粹就是當前 ts 文件內的代碼不想運行在全局命名空間內,所以可以通過 命名空間的語法,讓其運行在指定的命名空間內,防止污染全局變數。
語法:
namespace Validation {
//...
}
其他
本篇只講了 TypeScript 的一些基礎語法,還有其他更多知識點,比如引入三方不是用 TypeScript 寫的庫時需要編寫的 .d.ts 聲明文件,比如編譯配置文件的各種配置項,比如枚舉,更多更多的內容,請參考開頭聲明部分給出的 TypeScript 中文網連接。
大家好,我是 dasu,歡迎關註我的公眾號(dasuAndroidTv),公眾號中有我的聯繫方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關註,要標明原文哦,謝謝支持~