從 JavaScript 到 TypeScript

牧云云發表於2017-07-02

本文首發在我的個人部落格:muyunyun.cn/posts/66a54…
文中的案例程式碼已經上傳到 TypeScript

TypeScript 並不是一個完全新的語言, 它是 JavaScript 的超集,為 JavaScript 的生態增加了型別機制,並最終將程式碼編譯為純粹的 JavaScript 程式碼。

TypeScript 簡介

TypeScript 由 Microsoft(算上 Angular 2 的話加上 Google)開發和維護的一種開源程式語言。 它支援 JavaScript 的所有語法和語義,同時通過作為 ECMAScript 的超集來提供一些額外的功能,如型別檢測和更豐富的語法。下圖顯示了 TypeScript 與 ES5,ES2015,ES2016 之間的關係。

使用 TypeScript 的原因

JavaScript 是一門弱型別語言,變數的資料型別具有動態性,只有執行時才能確定變數的型別,這種後知後覺的認錯方法會讓開發者成為除錯大師,但無益於程式設計能力的提升,還會降低開發效率。TypeScript 的型別機制可以有效杜絕由變數型別引起的誤用問題,而且開發者可以控制對型別的監控程度,是嚴格限制變數型別還是寬鬆限制變數型別,都取決於開發者的開發需求。新增型別機制之後,副作用主要有兩個:增大了開發人員的學習曲線,增加了設定型別的開發時間。總體而言,這些付出相對於程式碼的健壯性和可維護性,都是值得的。

此外,型別註釋是 TypeScript 的內建功能之一,允許文字編輯器和 IDE 可以對我們的程式碼執行更好的靜態分析。 這意味著我們可以通過自動編譯工具的幫助,在編寫程式碼時減少錯誤,從而提高我們的生產力。

對 TypeScript 的簡介到此,接下來對其特有的知識點進行簡單概括總結,(網上很多教程實際上把 ES6, ES7 的知識點也算進 ts 的知識點了,當然這沒錯~)

資料型別

String 型別

一個儲存字串的文字,型別宣告為 string。可以發現型別宣告可大寫也可小寫,後文同理。

let name: string = 'muyy'
let name2: String = 'muyy'複製程式碼

Boolen 型別

boolean是 true 或 false 的值,所以 let isBool3: boolean = new Boolean(1) 就會編譯報錯,因為 new Boolean(1) 生成的是一個 Bool 物件。

let isBool1: boolean = false複製程式碼

Number 型別

let number: number = 10;複製程式碼

Array 型別

陣列是 Array 型別。然而,因為陣列是一個集合,我們還需要指定在陣列中的元素的型別。我們通過 Array<type> or type[] 語法為陣列內的元素指定型別

let arr:number[] = [1, 2, 3, 4, 5];
let arr2:Array<number> = [1, 2, 3, 4, 5];

let arr3:string[] = ["1","2"];
let arr4:Array<string> = ["1","2"];複製程式碼

Enums 型別

列出所有可用值,一個列舉的預設初始值是0。你可以調整一開始的範圍:

enum Role {Employee = 3, Manager, Admin}
let role: Role = Role.Employee
console.log(role) // 3複製程式碼

Any 型別

any 是預設的型別,其型別的變數允許任何型別的值:

let notSure:any = 10;
let notSure2:any[] = [1,"2",false];複製程式碼

Void 型別

JavaScript 沒有空值 Void 的概念,在 TypeScirpt 中,可以用 void 表示沒有任何返回值的函式:

function alertName(): void {
  console.log('My name is muyy')
}複製程式碼

函式

為函式定義型別

我們可以給每個引數新增型別之後再為函式本身新增返回值型別。 TypeScript能夠根據返回語句自動推斷出返回值型別,因此我們通常省略它。下面函式 add, add2, add3 的效果是一樣的,其中是 add3 函式是函式完整型別。

function add(x: string, y: string): string{
    return "Hello TypeScript";
}

let add2 = function(x: string, y: string): string{
    return "Hello TypeScript";
}

let add3: (x: string, y: string) => string = function(x: string, y: string): string{
    return "Hello TypeScript";
}複製程式碼

可選引數和預設引數

JavaScript 裡,每個引數都是可選的,可傳可不傳。 沒傳參的時候,它的值就是 undefined 。 在 TypeScript 裡我們可以在引數名旁使用?實現可選引數的功能。 比如,我們想讓 lastname 是可選的:

function buildName(firstName: string, lastname?: string){
    console.log(lastname ? firstName + "" + lastname : firstName)
}

let res1 = buildName("鳴","人"); // 鳴人
let res2 = buildName("鳴"); // 鳴
let res3 = buildName("鳴", "人", "君"); // Supplied parameters do not match any signature of call target.複製程式碼

如果帶預設值的引數出現在必須引數前面,使用者必須明確的傳入 undefined 值來獲得預設值。 例如,我們重寫上例子,讓 firstName 是帶預設值的引數:

function buildName2(firstName = "鳴", lastName?: string){
    console.log(firstName + "" + lastName)
}

let res4 = buildName2("人"); // undefined人
let res5 = buildName2(undefined, "人"); // 鳴人複製程式碼

傳統的JavaScript程式使用函式和基於原型的繼承來建立可重用的元件,但對於熟悉使用物件導向方式的程式設計師來講就有些棘手,因為他們用的是基於類的繼承並且物件是由類構建出來的。 從ECMAScript 2015,也就是ECMAScript 6開始,JavaScript程式設計師將能夠使用基於類的物件導向的方式。 使用TypeScript,我們允許開發者現在就使用這些特性,並且編譯後的JavaScript可以在所有主流瀏覽器和平臺上執行,而不需要等到下個JavaScript版本。

class Person{
    name:string; // 這個是對後文this.name型別的定義
    age:number;
    constructor(name:string,age:number){
        this.name = name;
        this.age = age;
    }
    print(){
        return this.name + this.age;
    }
}

let person:Person = new Person('muyy',23)
console.log(person.print()) // muyy23複製程式碼

我們在引用任何一個類成員的時候都用了 this。 它表示我們訪問的是類的成員。其實這本質上還是 ES6 的知識,只是在 ES6 的基礎上多上了對 this 欄位和引用引數的型別宣告。

繼承

class Person{
    public name:string;  // public、private、static 是 typescript 中的類訪問修飾符
    age:number;
    constructor(name:string,age:number){
        this.name = name;
        this.age = age;
    }
    tell(){
        console.log(this.name + this.age);
    }
}

class Student extends Person{
    gender:string;
    constructor(gender:string){
        super("muyy",23);
        this.gender = gender;
    }
    tell(){
        console.log(this.name + this.age + this.gender);
    }
}

var student = new Student("male");
student.tell();  // muyy23male複製程式碼

這個例子展示了 TypeScript 中繼承的一些特徵,可以看到其實也是 ES6 的知識上加上型別宣告。不過這裡多了一個知識點 —— 公共,私有,以及受保護的修飾符。TypeScript 裡,成員預設為 public ;當成員被標記成 private 時,它就不能在宣告它的類的外部訪問;protected 修飾符與private 修飾符的行為很相似,但有一點不同,protected 成員在派生類中仍然可以訪問。

儲存器

TypeScript 支援通過 getters/setters 來擷取對物件成員的訪問。 它能幫助你有效的控制對物件成員的訪問。

對於存取器有下面幾點需要注意的:
首先,存取器要求你將編譯器設定為輸出 ECMAScript 5 或更高。 不支援降級到 ECMAScript 3。 其次,只帶有 get 不帶有 set 的存取器自動被推斷為 readonly。 這在從程式碼生成 .d.ts 檔案時是有幫助的,因為利用這個屬性的使用者會看到不允許夠改變它的值。

class Hello{
    private _name: string;
    private _age: number;
    get name(): string {
        return this._name;
    }
    set name(value: string) {
        this._name = value;
    }
    get age(): number{
        return this._age;
    }
    set age(age: number) {
        if(age>0 && age<100){
            console.log("年齡在0-100之間"); // 年齡在0-100之間
            return;
        }
        this._age = age;
    }
}

let hello = new Hello();
hello.name = "muyy";
hello.age = 23
console.log(hello.name); // muyy複製程式碼

介面

介面

TypeScript的核心原則之一是對值所具有的結構進行型別檢查。在TypeScript裡,介面的作用就是為這些型別命名和為你的程式碼或第三方程式碼定義契約。

interface LabelValue{
    label: string;
}

function printLabel(labelObj: LabelValue){
    console.log(labelObj.label);
}

let myObj = {
    "label":"hello Interface"
};
printLabel(myObj);複製程式碼

LabelledValue 介面就好比一個名字,它代表了有一個 label 屬性且型別為 string 的物件。只要傳入的物件滿足上述必要條件,那麼它就是被允許的。

另外,型別檢查器不會去檢查屬性的順序,只要相應的屬性存在並且型別也是對的就可以。

可選屬性

帶有可選屬性的介面與普通的介面定義差不多,只是在可選屬性名字定義的後面加一個 ? 符號。可選屬性的好處之一是可以對可能存在的屬性進行預定義,好處之二是可以捕獲引用了不存在的屬性時的錯誤。

interface Person{
    name?:string;
    age?:number;
}

function printInfo(info:Person){
    console.log(info);
}

let info = {
    "name":"muyy",
    "age":23
};

printInfo(info); // {"name": "muyy", "age": 23}

let info2 = {
    "name":"muyy"
};

printInfo(info2); // {"name": "muyy"}複製程式碼

函式型別

介面能夠描述 JavaScript 中物件擁有的各種各樣的外形。 除了描述帶有屬性的普通物件外,介面也可以描述函式型別。定義的函式型別介面就像是一個只有引數列表和返回值型別的函式定義。引數列表裡的每個引數都需要名字和型別。定義後完成後,我們可以像使用其它介面一樣使用這個函式型別的介面。

interface SearchFunc{
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string,subString: string){
    return source.search(subString) !== -1;
};

console.log(mySearch("鳴人","鳴")); // true
console.log(mySearch("鳴人","纓")); // false複製程式碼

可索引型別

與使用介面描述函式型別差不多,我們也可以描述那些能夠“通過索引得到”的型別,比如 a[10]ageMap["daniel"]。 可索引型別具有一個索引簽名,它描述了物件索引的型別,還有相應的索引返回值型別。 讓我們看如下例子:

interface StringArray{
    [index: number]: string;
}

let MyArray: StringArray;
MyArray = ["是","雲","隨","風"];
console.log(MyArray[2]); // 隨複製程式碼

類型別

與 C# 或 Java 裡介面的基本作用一樣,TypeScript 也能夠用它來明確的強制一個類去符合某種契約。

我們可以在介面中描述一個方法,在類裡實現它,如同下面的 setTime 方法一樣:

interface ClockInterface{
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface{
    currentTime: Date;
    setTime(d: Date){
        this.currentTime = d;
    }
    constructor(h: number, m: number) {}
}複製程式碼

繼承介面

和類一樣,介面也可以相互繼承。 這讓我們能夠從一個介面裡複製成員到另一個介面裡,可以更靈活地將介面分割到可重用的模組裡。

interface Shape{
    color: string;
}

interface PenStroke{
    penWidth: number;
}

interface Square extends Shape,PenStroke{
    sideLength: number;
}

let s = <Square>{};
s.color = "blue";
s.penWidth = 100;
s.sideLength = 10;複製程式碼

模組

TypeScript 與 ECMAScript 2015 一樣,任何包含頂級 import 或者 export 的檔案都被當成一個模組。

export interface StringValidator{
    isAcceptable(s:string): boolean;
}

var strReg = /^[A-Za-z]+$/;
var numReg = /^[0-9]+$/;

export class letterValidator implements StringValidator{
    isAcceptable(s:string): boolean{
        return strReg.test(s);
    }
}

export class zipCode implements StringValidator{
    isAcceptable(s: string): boolean{
        return s.length == 5 && numReg.test(s);
    }
}複製程式碼

泛型

軟體工程中,我們不僅要建立一致的定義良好的 API ,同時也要考慮可重用性。 元件不僅能夠支援當前的資料型別,同時也能支援未來的資料型別,這在建立大型系統時為你提供了十分靈活的功能。
在像 C# 和 Java 這樣的語言中,可以使用泛型來建立可重用的元件,一個元件可以支援多種型別的資料。 這樣使用者就可以以自己的資料型別來使用元件。

初探泛型

如下程式碼,我們給 Hello 函式新增了型別變數 T ,T 幫助我們捕獲使用者傳入的型別(比如:string)。我們把這個版本的 Hello 函式叫做泛型,因為它可以適用於多個型別。 程式碼中 outputoutput2 是效果是相同的,第二種方法更加普遍,利用了型別推論 —— 即編譯器會根據傳入的引數自動地幫助我們確定T的型別:

function Hello<T>(arg:T):T{
    return arg;
}

let outPut = Hello<string>('Hello Generic');
let output2 = Hello('Hello Generic')

console.log(outPut);
console.log(outPut2);複製程式碼

參考資料

相關文章