1. 程式人生 > 其它 >你應該知道的TypeScript高階概念

你應該知道的TypeScript高階概念

介面

例如我們這定義一個叫做printPost的函式,那這個函式可以接收一個文章物件引數post,然後在函式的內部去列印文章的title, 然後再去列印他的content屬性。

function printPost (post) {
    console.log(post.title);
    console.log(post.content);
}

那這個時候對於這個函式所接收的post物件他就有一定的要求,也就是我們所傳入的這個物件必須要存在一個title屬性和一個content屬性,只不過這種要求他實際上是隱性的,他沒有明確的表達出來。

那這種情況下我們就可以使用介面去表現出來這種約束,這裡我們可以嘗試先去定義一個介面。

定義介面的方式呢就是使用interface這樣一個關鍵詞,然後後面跟上介面的名稱,這裡我們可以叫做post,然後就是一對{},然後{}裡面就可以新增具體的成員限制。這裡我們新增一個title和content,型別都是string。

interface Post {
    title: string;
    content: string;
}

注意這裡我們可以使用逗號分割成員,但是更標準的做法是使用分號去分割,而且呢這個分號跟js中絕大多數的分號是一樣的,可以省略,那關於是否應該在程式碼當中明確使用每一個分號,個人的編碼習慣是不加,你可以根據你所在的團隊或者是專案對應的編碼規範來去決定要不要加分號,這個問題我們不做過多討論。

完成過後我們這裡可以給這個post引數的型別設定為我們剛剛所定義的Post介面。

function printPost (post: Post) {
    console.log(post.title);
    console.log(post.content);
}

printPost({
    title: 'hello',
    content: 'typescript'
})

那此時就是顯示的要求我們所傳入的物件他必須要有title和content這兩個成員了,那這就是介面的一個基本作用。

一句話去總結,介面就是用來約束物件的結構,那一個物件去實現一個介面,他就必須要去擁有這個介面當中所約束的所有的成員。

我們可以編譯一下這個程式碼,編譯過後我們開啟對應的js檔案,我們在js當中並不會發現有任何跟介面相關的程式碼,也就是說TypeScript中的介面他只是用來為我們有結構的資料去做型別約束的,在實際執行階段呢,實際這種介面他並沒有意義。

可選成員,只讀成員

對於介面中約定的成員,還有一些特殊的用法,我們依次來看一下。

首先是可選成員,如果說我們在一個物件當中,我們某一個成員他是可有可無的話,那這樣的話我們對於約束這個物件的介面來說我們可以使用可選成員這樣一個特性。

例如我們這裡新增一個subTitle這樣一個成員,他的型別同樣是string,不過我們這裡的文章不一定是每一個都有subTitle,這種況下我們就可以在subTitle後面新增一個問號,這就表示我們這個subTitle成員他是可有可無的了。

interface Post {
    title: string;
    content: string;
    subTitle?: string;
}

那這種用法呢其實就是相當於給這個subTitle標記他的型別是string或者是undefined。這就是可選成員。

接下來我們再來看一下只讀成員這樣一個特性,那這裡我們再給Post介面新增一個summary這樣一個成員,那一般邏輯上來講的話文章的summary他都是從文章的內容當中自動提取出來的,所以說我們不應該允許外界去設定他。

那這種情況下我們可以使用readonly這樣一個關鍵詞,去修飾一下這裡的summary。那添加了readonly過後我們這個summary他在初始化完成過後就不能夠再去修改了。如果我們再去修改就會報錯。這就是隻讀成員。

interface Post {
    title: string;
    content: string;
    subTitle?: string;
    readonly summary: string;
}

最後我們再來看一個動態成員的用法,那這種用法一般是適用於一些具有動態成員物件,例如程式當中的快取物件,那他在執行過程中就會出現一些動態的鍵值。

這裡我們來新建一個新的介面,因為我們在定義的時候我們是無法知道會有那些具體的成員,所以說我們就不能夠去指定,具體的成員名稱,而是使用一個[], 這個[]中使用key: string。

這個key並不是固定的,可以是任意的名稱, 只是代表了我們屬性的名稱,他是一個格式,然後後面這個string就是成員名的型別,也就是鍵的型別,後面我們可以跟上動態屬性的值為string。

interface Cache {
    [key: string]: string;
}

完成以後我們再來建立一個cache物件,讓他去實現這個介面,那這個時候我們就可以在這個cache物件上動態的去新增任意的成員了, 只不過這些成員他都必須是stringd型別的鍵值。

const cache: Cache = {};

cache.foo = 'value1';
cache.bar = 'value2'

類的基本使用

類可以說是面向物件程式設計中一個最重要的概念,關於類的作用這裡我們再簡單描述一下。他就是用來描述一類具體事物的抽象特徵,我們可以以生活角度去舉例。

例如手機就屬於一個型別,那這個型別的特徵呢就是能夠打電話,發信息。那在這個型別下面呢他還會有一些細分的子類,那這種子類他一定會滿足父類的所有特徵,然後再多出來一些額外的特徵。例如只能手機,他除了可以打電話發簡訊還能夠使用一些app。

那我們是不能直接去使用類的,而是去使用這個類的具體事物,例如你手中的只能手機。

那類比到程式的角度,類也是一樣的,他可以用來去描述一類具體物件的一些抽象成員,那在ES6以前,JavaScript都是通過函式然後配合原型去模擬實現的類。那從ES6開始,JavaScript中有了專門的class。

而在TypeScript中,我們除了可以使用所有ECMAScript的標準當中所有類的功能,他還添加了一些額外的功能和用法,例如我們對類成員有特殊的訪問修飾符,還有一些抽象類的概念。

那對於ECMAScript標準中的class,我們這裡就不單獨去介紹了,如果你不太熟悉,你可以去參考ECMAScript前面的文章。

這裡我們來著重介紹一下,在TypeScript中額外多出來的一些新的類的一些特性,我們可以先來宣告一個叫做Person的型別,然後我們在這個型別當中去宣告一下constructor構造桉樹,在建構函式當中我們接收一個name和age引數,那這裡我們仍然可以使用型別註解的方式去標註我們這個地方每個引數的型別。

然後在這個建構函式的裡面我們可以使用this去為當前這個型別的屬性去賦值,不過這裡直接去使用this訪問當前類的屬性會報錯,說的是當前這個Person型別上面並不存在對應的name和age。

class Person {
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

這是因為在TypeScript中我們需要明確在型別中取宣告他所擁有的一些屬性,而不是直接在建構函式當中動態通過this去新增。

那在型別宣告屬性的方式就是直接在類當中去定義, 那這個語法呢是ECMAScript2016標準當中定義的,那我們同樣可以在這裡給name和gae屬性新增型別。那他也可以通過等號去直接賦值一個初始值,不過一般情況下我們還是會在建構函式中動態的為屬性賦值。

需要注意的是,在TypeScript中類的屬性他必須要有一個初始值,可以在等號後面去賦值,或者是在建構函式當中去初始化,兩者必須做其一,否則就會報錯。

class Person {
    name: string = 'init name';
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

那以上就是在TypeScript中類屬性在定義上的一些細微差異,其實具體來說就是我們類的屬性他在使用之前必須要現在型別當中去宣告,那這麼做的目的其實就是為了給我們的屬性去做一些型別的標註。

那除此之外呢我們仍然可以按照ES6標準當中的語法,為這個型別去宣告一些方法,例如我們這裡新增一個叫做sayHi的方法,那在這個方法當中我們仍然可以使用函式型別註解的方式去限制引數的型別和返回值的型別。

那在這個方法的內部呢我們同樣可以使用this去訪問當前例項物件,也就可以訪問到對應的屬性。

class Person {
    name: string = 'init name';
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    sayHi(msg: string): void {
        console.log(`I am ${this.name}, ${msg}`);
    }
}

那以上就是類在TypeScript中的一個基本使用。

類的訪問修飾符

接下來我們再來看幾個TypeScript中類的一些特殊用法,那首先就是類當中成員的訪問修飾符,類中的每一個成員都可以使用訪問修飾符去修飾他們。

例如我們這裡給age屬性前面去新增一個private,表示這個age屬性是一個私有屬性,這種私有屬性只能夠在類的內部去訪問,這裡我們建立一個Person物件, 我們列印tom的name屬性和age屬性。可以發現name可以訪問,age就會報錯,因為age已經被我們標記為了私有屬性。

class Person {
    name: string = 'init name';
    private age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    sayHi(msg: string): void {
        console.log(`I am ${this.name}, ${msg}`);
    }
}

const tom = new Person('tom', 18);

console.log(tom.name);
console.log(tom.age);

除了private以外,我們還可以使用public修飾符去修飾成員,意思是他是一個共有成員,不過再TypeScript中,類成員的訪問修飾符預設就是public,所以我們這裡加不加public效果都是一樣的。不過我們還是建議大家手動去加上這種public的修飾符,因為這樣的話,我們的程式碼會更加容易理解一點。

class Person {
    public name: string = 'init name';
    private age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    sayHi(msg: string): void {
        console.log(`I am ${this.name}, ${msg}`);
    }
}

最後還有一個叫做protected修飾符,說的就是受保護的,我們可以新增一個gender的屬性,他的訪問修飾符我們就使用protected,我們同樣在建構函式中初始化一下gender。

完成過後我們在例項物件上去訪問gender,會發現也是訪問不到的,也就是protected也不能在外部直接訪問。

class Person {
    public name: string = 'init name';
    private age: number;
    protected gender: boolean;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.gender = true;
    }

    sayHi(msg: string): void {
        console.log(`I am ${this.name}, ${msg}`);
    }
}

const tom = new Person('tom', 18);

console.log(tom.name);
// console.log(tom.age);
console.log(tom.gender);

那他跟private的區別到底在什麼地方呢, 這裡我們可以再定義一個叫做Student的型別,我們讓這個型別去繼承自Person,我們在建構函式中嘗試訪問父類的gender,是可以訪問到的。那意思就是protected只允許在子類當中去訪問對應的成員。

class Student extends Person {
    constructor(name: string, age: number) {
        super(name, age); // 子類需要呼叫super將引數傳給父類。
        console.log(this.gender);
    }
}

那以上就是TypeScript當中對於類額外新增的三個訪問修飾符。分別是private,protected和public,那他們的作用可以用來去控制類當中的成員的可訪問級別。

那這裡還有一個需要注意的點,就是對於建構函式的訪問修飾符,那建構函式的訪問修飾符預設也是public,那如果說我們把它設定為private,那這個型別就不能夠在外部被例項化了,也不能夠被繼承,那在這樣一種情況下,我們就只能夠在這個類的內部去新增一個靜態方法,然後在靜態方法當中去建立這個型別的例項,因為private只允許在內部訪問。

例如我們這裡再去新增一個叫create的靜態方法,那static也是ES6標準當中定義的,然後我們就可以在這個create方法中去使用new的方式去建立這個型別的例項,因為new的方式就是呼叫了這個型別的建構函式。

此時我們就可以在外部去使用create靜態方法建立Student型別的物件了。

class Student extends Person {
    private constructor(name: string, age: number) {
        super(name, age); // 子類需要呼叫super將引數傳給父類。
        console.log(this.gender);
    }

    static create(name: string, age: number) {
        return new Student(name, age);
    }
}

const jack = Student.create('jack', 18);

那如果我們把建構函式標記為protected,這樣一個型別也是不能在外面被例項化的,但是相比於private他是允許繼承的,這裡我們就不單獨演示了。

類的只讀屬性

對屬性成員我們除了可以使用private和protected去控制它的訪問級別,我們還可以使用一個叫做readonly的關鍵詞去把這個成員設定為只讀的。

這裡我們將gender屬性設定為readonly,注意這裡如果說我們的屬性已經有了訪問修飾符的話,那readonly應該跟在訪問修飾符的後面,對於只讀屬性,我們可以選擇在型別宣告的時候直接通過等號的方式去初始化,也可以在建構函式當中去初始化,二者只能選其一。

也就是說我們不能在宣告的時候初始化,然後在建構函式中修改它,因為這樣的話已經破壞了readonly。

class Person {
    public name: string = 'init name';
    private age: number;
    protected readonly gender: boolean;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
        this.gender = true;
    }

    sayHi(msg: string): void {
        console.log(`I am ${this.name}, ${msg}`);
        console.log(this.age);
    }
}

const tom = new Person('tom', 18);

console.log(tom.name);

在初始化過後呢, 這個gender屬性就不允許再被修改了,無論是在內部還是外部,他都是不允許修改的。

以上就是readonly這樣一個只讀屬性,還是比較好理解的。

類與介面

相比於類,介面的概念要更為抽象一點,我們可以接著之前所說的手機的例子來去做比。我們說手機他是一個型別,這個例項的型別都是能夠打電話,發簡訊的,因為手機這個類的特徵呢就是打電話,發簡訊。

但是呢,我們能夠打電話的,不僅僅只有手機,在以前還有比較常見的座機,也能夠打電話,但是座機並不屬於手機這個類目,而是一個單獨的類目,因為他不能夠發簡訊,也不能夠拿著到處跑。

那在這種情況下就會出現,不同的類與類之間也會有一些共同的特徵,那對於這些公共的特徵我們一般會使用介面去抽象,那你可以理解為手機也可以打電話,因為他實現了能夠打電話的協議,而座機也能夠打電話因為他也實現了這個相同的協議。

那這裡所說的協議呢我們在程式當中就叫做介面,當然如果說你是第一次接受這種概念的話,那可能理解起來會有些吃力,個人的經驗就是多思考,多從生活的角度去想,如果實在想不通,更粗暴的辦法就是不斷的去用,用的過程當中慢慢的去總結規律時間長了自然也就好了。

class Person {
    eat (food: string): void {}
    run (distance: number) {}
}

class Animal {
    eat (food: string): void {}
    run (distance: number) {}
}

這裡我們來看一個例子,我們定義好了兩個型別,分別是Person和Animal,也就是人類和動物類,那他們實際上是兩個完全不同的型別,但是他們之間也會有一些相同的特性。例如他們都會吃東西都會跑。

那這種情況下就屬於不同的型別實現了一個相同的介面,那可能有人會問,我們為什麼不給他們之間抽象一個公共的父類,然後把公共的方法都定義到父類當中。

那這個原因也很簡單,雖然人和動物都會吃,都會跑,但是我們說人吃東西和狗吃東西能是一樣的麼,那他們只是都有這樣的能力,而這個能力的實現肯定是不一樣的。

那在這種情況下我們就可以使用介面去約束這兩個型別之間公共的能力,我們定義一個介面叫做EatAndRun,然後我們在這個介面當中分別去新增eat和run這兩個方法成員的約束。

那這裡我們就需要使用這種函式簽名的方式去約束這兩個方法的型別,而我們這裡是不做具體的方法實現的。

interface EatAndRun {
    eat (food: string): void;
    run (distance: number): void;
}

那有了這個介面過後呢,我們再到Person型別後面,我們使用implements實現以下這個EatAndRun介面, 那此時在我們這個型別中就必須要有對應的成員,如果沒有就會報錯,因為我們實現這個介面就必須有他對應的成員。

class Person implements EatAndRun {
    eat (food: string): void {}
    run (distance: number) {}
}

那這裡我們需要注意的一點是,在C#和Java這樣的一些語言當中他建議我們儘可能讓每一個介面的定義更加簡單,更加細化。就是我們EatAndRun這樣一個介面當中我們抽象了兩個方法,那就相當於抽象了兩個能力,那這兩個能力必然會同時存在麼?是不一定的。

例如說摩托車也會跑,但是就不會吃東西,所以說我們更為合理的就是一個介面只去約束一個能力,然後讓一個型別同時去實現多個介面,那這樣的話會更加合理一些,那我們這裡可以把這個介面拆成一個Eat介面和Run介面,每個介面只有一個成員。

然後我們就可以在型別的後面使用逗號的方式,同時去實現Eat和Run這兩個介面。

interface Eat {
    eat (food: string): void;
}

interface Run {
    run (distance: number): void;
}

class Person implements Eat, Run {
    eat (food: string): void {}
    run (distance: number) {}
}

那以上呢就是用介面去對類進行一些抽象,那這裡再多說一句題外話,就是大家千萬不要把自己框死在某一門語言或者是技術上面,最好可以多接觸,多學習一些周邊的語言或者技術,因為這樣的話可以補充你的知識體系。

那最簡單來說,一個只瞭解JavaScript的開發人員,即便說他對JavaScript有多麼精通,那他也不可能設計出一些比較高階的產品。

例如我們現在比較主流的一些框架,他們大都採用一些MVVM的這樣一些思想,那這些思想呢他實際上最早出現在微軟的WPS技術當中的,如果說你有更寬的知識面的話,那你可以更好的把多家的思想融合到一起,所以說我們的視野應該放寬一點。

抽象類

最後我們再來了解一下抽象類,那抽象類在某種程度上來說跟介面有點類似,那他也是用來約束子類當中必須要有某一個成員。

但是不同於介面的是,抽象類他可以包含一些具體的實現,而介面他只能夠是一個成員的一個抽象,他不包含具體的實現。

那一般比較大的類目我們都建議大家使用抽象類,例如我們剛剛所說的動物類,那其實他就應該是抽象的,因為我們所說的動物他只是一個泛指,他並不夠具體,那在他的下面一定會有一些更細化的劃分,比如說小狗,小貓之類的。

而且我們在生活當中一般都會說我們買了一條狗,或者說買了一隻貓,從來沒有人說我們買了一個動物。

abstract class Animal {
    eat (food: string): void {}
}

我們有一個Animal型別,那他應該像我們剛剛說的那樣,被定義成抽象類,定義抽象類的方式就是在class關鍵詞前面新增一個abstract,那這個型別被定義成抽象類過後,他就只能夠被繼承,不能夠再去例項化。

在這種情況下我們就必須使用子類去繼承這個抽象類, 這裡我們定義一個叫做Dog的型別, 然後我們讓他繼承自Animal,那在抽象類當中我們還可以去定義一些抽象方法,那這種抽象方法我們可以使用abstract關鍵詞來修飾,我們這裡定義一個叫做run的抽象方法, 需要注意的是抽象方法也不需要方法體.

當父類中有抽象方法時,我們的子類就必須要去實現這個方法。

那此時我們再去使用這個子類所建立的物件時,就會同時擁有父類當中的一些例項方法以及自身所實現的方法。那這就是抽象類的基本使用。

abstract class Animal {
    eat (food: string): void {}
    abstract run (distance: number): void;
}

class Dog extends Animal {
    run (distance: number): void {}
}

關於抽象類呢,更多的還是去理解他的概念,他在使用上並沒有什麼複雜的地方。

泛型

泛型(Generics)是指在定義函式、介面或者類的時候, 不預先指定其型別,而是在使用是手動指定其型別的一種特性。

比如我們需要建立一個函式, 這個函式會返回任何它傳入的值。

function identity(arg: any): any {
  return arg
}

identity(3) // 3

這代程式碼編譯不會出錯,但是存在一個顯而易見的缺陷, 就是沒有辦法約束輸出的型別與輸入的型別保持一致。

這時,我們可以使用泛型來解決這個問題;

function identity<T>(arg: T): T {
  return arg
}

identity(3) // 3

我們在函式名後面加了 , 其中的 T 表示任意輸入的型別, 後面的 T 即表示輸出的型別,且與輸入保持一致。

當然我們也可以在呼叫時手動指定輸入與輸出的型別, 如上述函式指定 string 型別:

identity<number>(3) // 3

在泛型函式內部使用型別變數時, 由於事先並不知道它是那種型別, 所以不能隨意操作它的屬性和方法:

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length)   // err 
  return arg
}

上述函式中 型別 T 上不一定存在 length 屬性, 所以編譯的時候就報錯了。

這時,我們可以的對泛型進行約束,對這個函式傳入的值約束必須包含 length 的屬性, 這就是泛型約束:

interface lengthwise {
  length: number
}

function loggingIdentity<T extends lengthwise>(arg: T): T {
  console.log(arg.length)   // err 
  return arg
}

loggingIdentity({a: 1, length: 1})  // 1
loggingIdentity('str') // 3
loggingIdentity(6) // err  傳入是引數中未能包含 length 屬性

這樣我們就可以通過泛型約束的方法對函式傳入的引數進行約束限制。

多個引數時也可以在泛型約束中使用型別引數 如你聲明瞭一個型別引數, 它被另一型別引數所約束。現在想要用屬性名從物件裡湖區這個屬性。並且還需確保這個屬性存在於這個物件上, 因此需要咋這兩個型別之間使用約束,

簡單舉例來說: 定義一個函式, 接受兩個引數 第一個是個物件 obj,第二個個引數是第一引數 key 是物件裡面的鍵名, 需要輸入 obj[key]

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}

let obj = { a: 1, b: 2, c: 3 }

getProperty(obj, 'a') // success
getProperty(obj, 'm') // err obj 中不存在 m 這個引數

我們可以為泛型中的型別引數指定預設型別。當使用泛型時沒有在程式碼中直接指定型別引數,從實際值引數中也無法推測出時,這個預設型別就會起作用

function createArr<T = string>(length: number, value: T): Array<T> {
  let result: T[] = []
  for( let i = 0; i < lenght; i++ ) {
    result[i] = value
  }
  return result
}

簡單來說,泛型就是把我們定義時不能夠明確的型別變成一個引數,讓我們在使用的時候再去傳遞這樣一個型別引數。

https://www.houdianzi.com/ logo設計公司

型別宣告

在專案開發過程中我們難免會用到一些第三方的npm模組,而這些npm模組他不一定都是通過TypeScript編寫的,所以說他所提供的成員呢就不會有強型別的體驗。

比如我們這裡安裝一個lodash的模組,那這個模組當中就提供了很多工具函式,安裝完成過後我們回到程式碼當中,我們使用ES Module的方式import匯入這個模組,我們這裡匯入的時候TypeScript就已經報出了錯誤,找不到型別宣告的檔案,我們暫時忽略。

這裡我們提取一下camelCase的函式,那這個函式的作用就是把一個字串轉換成駝峰格式,那他的引數應該是一個string,返回值也應該是一個string,但是我們在呼叫這個函式逇時候並沒有任何的型別提示。

import { camelCase } from 'lodash';

const res = camelCase('hello typed');

那在這種情況下我們就需要單獨的型別宣告,這裡我們可以使用detar語句來去宣告一下這個函式的型別,具體的語法就是declare function 後面跟上函式簽名, 引數型別是input,型別是string,返回值也應該是string

declare function camelCase (input: string ): string;

那有了這樣一個宣告過後,我們再去使用這個camelCase函式。這個時候就會有對應的型別限制了。

那這就是所謂的型別宣告,說白了就是一個成員他在定義的時候因為種種原因他沒有宣告一個明確的型別,然後我們在使用的時候我們可以單獨為他再做出一個明確的宣告。

那這種用法存在的原因呢,就是為了考慮相容一些普通的js模組,由於TypeScript的社群非常強大,目前一些比較常用的npm模組都已經提供了對應的宣告,我們只需要安裝一下它所對應的這個型別宣告模組就可以了。

lodash的報錯模組已經提示了,告訴我們需要安裝一個@types/lodash的模組,那這個模組其實就是我們lodash所對應的型別宣告模組,我們可以安裝這個模組。安裝完成過後,lodash模組就不會報錯了。在ts中.d.ts檔案都是做型別宣告的檔案,

除了型別宣告模組,現在越來越多的模組已經在內部繼承了這種型別的宣告檔案,很多時候我們都不需要單獨安裝這種型別宣告模組。

例如我們這裡安裝一個query-string的模組,這個模組的作用就是用來去解析url當中的query-string字串,那在這個模組當中他就已經包含了型別宣告檔案。

我們這裡直接匯入這個模組,我們所匯入的這個物件他就直接會有型別約束。

那以上就是對TypeScript當中所使用第三方模組型別宣告的介紹。

那這裡我們再來總結一下,在TypeScript當中我們去引用第三方模組,如果這個模組當中不包含所對應的型別宣告檔案,那我們就可以嘗試去安裝一個所對應的型別宣告模組,那這個型別宣告模組一般就是@types/模組名。

那如果也沒有這樣一個對應的型別宣告模組,那這種情況下我們就只能自己使用declare語句去宣告所對應的模組型別,那對於declare詳細的語法這裡我們不再單獨介紹了,有需要的話可以單獨去查詢一下官方文件。