iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 18
2
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 18

Day 18. 機動藍圖・類別宣告 X 藍圖設計 - TypeScript Class

https://ithelp.ithome.com.tw/upload/images/20190918/20120614LXY0rJK4ya.png

閱讀本篇文章前,仔細想想看

複合型別 unionintersection 的功能與意義代表為何?

如果還沒理解完畢的話,可以先翻看前一篇文章喔!

今天總算要進到類別的主題了~ 不過有 OOP 經驗的人會覺得這部分應該算(超級)簡單。然而,講到一些進階主題,譬如策略模式或抽象類別可能就稍微難一些囉~

對於只熟悉 JavaScript 的開發者們(包含筆者本身喔!),對於 OOP 並不是完整的了解。此外,ES6 版本的類別語法 -- 儘管對於 JavaScript 朝向 OOP 方式開發已經算是很大的進展;不過對於筆者來說,ES6 Class 的機制並不是很完整,因為缺少了在 OOP 應用領域裡面很實用的功能 —— 而 TypeScript 則是把大部分類別應該出現的功能基本上實踐出來了

筆者除了嫌 ES6 Class 語法不完備外,對於未來出現的 Private Member(或者是俗稱的 Private Method,不過完整一點會稱為 Private Access Modifier)的語法也是深感覺得:『 醜 』一個字足以形容。(這是個人看法!)

(到正文開始前,以下部分讀者看不懂也沒關係,因為之後會講到 private 這東西到底在做什麼)

筆者在這裡很主觀,但忍不住還是給不知道的人看:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614UPW0JYLqYG.png

恩... 與其加一個 # 難道沒有想乾脆一點就換成 private 關鍵字嗎?

可能筆者的見識還不廣,也不知道當初提 Proposal 的程序為何?到底審核過程又是如何?要把一個新的 Feature 實作在 JS 引擎上的難度? —— 諸如此類的問題。(就當作是筆者在什麼東西也不懂吧!)

搞不好也有人認為加一個 # 很方便,但是對於學習一門語言而言,個人覺得只會增加複雜度,因為通常學一個語言版本是 A,另一個語言版本是 B —— 若 AB 語法機制相似的話,學完其中一個,另一個就會非常好上手,就算有細微差別也都可以再去花時間鑽研。(就像當初筆者使用 Ruby 以及一點點 C++ 打好類別相關的基礎,上手 TypeScript 自然就很快,相信很多開發者都有類似的經驗)

特別訂立出新的語法,必須要注重的是 -- 訂好這些語法規則之背後用意與哲學到底是什麼

如果單純只是研發更多 Syntax Sugar 的話而沒實質意義或質量性 —— 那筆者也只能說,就是該語言的一種噱頭而已,就看個人要不要使用囉。(糖果畢竟只是糖果,會不會容易蛀牙看個人造化

說到最後,Class 的完整概念在正式的 ECMAScript 裡是還不完備的,因此本系列文會從 Class 最基本的東西然後慢慢進行展開的動作。所以呢...

正文開始

TypeScript 類別的宣告與基本使用

環境建置

這裡我們要建第三個環境在本系列的 typescript-tutorial 的資料夾裡,畢竟前一個環境都是在講 interface 部分也是有點亂了。如果讀者有跟著本系列的範例的話,應該都知道筆者將這些程式碼存在哪裡。因此筆者就趕快將新的環境建好。

不過想要查看筆者所有的範例程式碼,可以到 GitHub —— Maxwell-Alexius/Iron-Man-Competition 這裡去查看喔~

這一次筆者ㄧ樣會在 typescript-tutorial 資料夾建造新的資料夾,名為 03-interface-class

$ cd PATH_TO/typescript-tutorial
$ mkdir 03-interface-class
$ cd ./03-interface-class

一如往常,讀者應該知道要下什麼東西來初始化 TypeScript 編譯器設定檔:

$ tsc --init

最後在該資料夾裡新增 index.ts 我們就準備好囉!(打開 VSCode 應該會出現如圖一的狀況)

$ touch index.ts

https://ithelp.ithome.com.tw/upload/images/20190918/20120614tSvOejEVtL.png
圖一:環境建立完成

最後還是得注意,把 tsconfig.json 裡面的 strictNullChecks 選項改成 true。筆者後續要開始示範小專案會再跟讀者說明這些編譯器設定到底在做什麼~

https://ithelp.ithome.com.tw/upload/images/20190918/201206143DiqOabHRh.png
圖二:將 tsconfig.json 裡的 strictNullChecks 設為 true

類別的概念:『 物件的藍圖 』

筆者先從最~最~最~最~最基本的東西開始,也就是(狹義的)物件本身。

貼心小提示

如果讀者是從這個篇章看起,本系列會用到一些特殊名詞,這是筆者自訂的,目的是要好解說本系列文章:

  • 狹義物件:泛指普通 JSON 物件
  • 廣義物件:包含狹義物件、函式、陣列、類別以及類別所建立出來的物件

通常一個 TypeScript 介面的宣告,就是要被作為:物件的規格。(讀者反嗆:“不然要做什麼?”

https://ithelp.ithome.com.tw/upload/images/20190918/20120614ILNGZWUfFZ.png

如果要讓物件可以做出什麼行為(Action),最簡單的做法就是在物件裡面定義方法(Method)。以剛剛的範例進行延伸的話,我們可以新增函式型別為介面裡其中一項規定的屬性 —— 要求開發者必須對該物件實踐出必要的方法。

https://ithelp.ithome.com.tw/upload/images/20190918/20120614dlvF3oC2jG.png

讀者看到這裡,想也知道 —— 又違反了 DRY(Don't Repeat Yourself)原則了啊!

貼心小提示

筆者寫到類別相關的文章會不停 DRY 來 DRY 去好色情的感覺),因此讀者要有心理準備。(這是哪門子的提示?

另外,我們也發現一件事情:printInfo 是該物件的方法,而每一個物件實作 printInfo 的程序都一模ㄧ樣,於是有了一個想法出現 —— 有沒有工具是跟介面的概念很相似,但是又可以預先把物件的屬性(Properties)與行為(Behaviours)都表現出來呢

於是上帝讓 類別 就這麼誕生了!(“讚美主!阿・們~!” <—— 關主 P 事,滾去寫你的程式!

類別(Class)的基本用法

筆者就馬上介紹如何宣告一個類別:

重點 1. 類別的宣告

若我們想要定義類別 C,其中類別包含屬性 P 與方法 M。其中,P 對應之型別為 Tp,而方法 M 對應之函式型別為 (paramName: Tparam): Toutput,則最基本的宣告方式為:

class C {
  P: Tp;
  
  M(paramName: Tparam): Toutput {
    // 方法內的內容
  }
}

備註:本宣告方式為最基本的宣告方式,並非所有類別都必須這樣定義的!

筆者綜觀本系列 —— 深深覺得感動,我們的主角從原本的型別 T 到介面 I,現在又多了類別 C 了。

貼心小提示

到現在還分不清函式(Function)和方法(Method)的人,可以參考這一篇

讀者可以看到,類別(Class)跟介面(Interface)的最大差別在於:

介面只能定義格式;而類別除了宣告屬性外,也可以描述出物件的行為

筆者示範:如果將 PersonInfo 介面(上一個範例)轉換成類別的宣告方式,大概會長什麼樣子。(TypeScript 判定以下程式碼的結果如圖三)

如果沒接觸過類別的讀者,剛開始進行學習類別時,會感到吃力的話是正常的。(筆者當初學的時候感到超級吃力,花了好幾個月的時間才知道類別到底怎麼寫。因為那時候學校課程上的是 C++,而 C++ 的類別難度對筆者而言,跟 TypeScript 相比簡直又是另一個層次啊;不過 TypeScript 的類別不會到特別難寫)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614AeGt9JV6ex.png

https://ithelp.ithome.com.tw/upload/images/20190918/201206141C1kGg7aXz.png
圖三:直接按照重點 1. 的方式將 PersonInfo 介面轉換成 CPersonInfo 類別,結果還是錯!

其實上面這段程式碼照樣會出錯!剛入門 TypeScript 的讀者可能在這一步感到疑惑,明明都已經把東西都標註好了,是還想怎樣啊?

那是因為跟 TypeScript 的型別推論機制(Type Inference)有關。但我們先不管這些錯誤!若筆者選擇直接講解解決錯誤的方法,又會對在 JS 圈打滾但不完全熟悉 OOP 概念的人太快。

先從基礎來!

先來個正式的名詞定義 —— 而這些名詞不管讀者跑到哪個 OOP 語言,不外乎都會看到這些名詞,想轉跑道或跳槽到其他 OOP 語言必備的名詞系列。

重點 2. 成員變數與方法 Member Variables & Member Methods

若某類別 C 裡,包含一系列屬性 P1P2、...、Pn 以及方法 M1M2、...、Mn。其中,類別 C 的宣告方式如下:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614N1jlgn7kex.png

我們將 P1P2、...、Pn 這一系列的屬性稱之為類別 C成員變數們(Member Variables)。

而我們也會將 M1M2、...、Mn 這一系列的方法稱之為類別 C成員方法們(Member Methods)。

貼心小提示

有些讀者可能會細問,為何不將 P1P2、...、Pn 取名為成員屬性,不都是物件的屬性嗎?為何都改成變數?

筆者這邊的回答是:在類別裡面,我們可以任意操作物件的屬性跟方法之間的關係屬性也可能會隨著物件方法的使用而被改變 —— 這個就是所謂的物件的變異(Mutation),而物件屬性變來變去的狀態跟變數的本質沒兩樣

筆者可以放心地用這些專有名詞了~

首先回到剛剛的錯誤。(筆者重新把錯誤貼在下面,如圖四)

https://ithelp.ithome.com.tw/upload/images/20190918/201206141C1kGg7aXz.png
圖四:直接按照重點 1. 的方式將 PersonInfo 介面轉換成 CPersonInfo 類別,結果還是錯!

我們的成員變數 —— 也就是 nameage 以及 hasPet 三個欄位都被 TS 標記有問題。那是因為它們三個都還沒被初始化,因此不符合各自的對應型別:stringnumberboolean

TypeScript 對類別可是很敏感的,所以其中一種解法是直接對那些欄位指派值。(以下程式碼被 TS 判定結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614INxHICoYFS.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614zo1hOePvUH.png
圖五:類別 CPersonInfo 裡的那些成員變數們的警告通通消失了

好,那既然錯誤都被解光光,我們就可以使用它啦~(請勿遐想,TypeScript 關心您

重點 3. 從類別建立物件的基本方式

若已宣告完類別 C,可以藉由 new 關鍵字從 C 建立出物件 O

let O = new C(/* 可能也會包含參數,會在建構子部分提及 */);

以下程式碼根據 CPersonInfo 建立出物件:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614uVxX202Dig.png

筆者編譯過後之執行結果如圖六。

https://ithelp.ithome.com.tw/upload/images/20190918/20120614o6CJYkxTtb.png
圖六:物件被建立起來了

從圖六可以看到,我們的物件被建立起來了!而且將它印出來的結果,確實有成員變數們的蹤跡。然而,成員方法能不能使用?(以下程式碼編譯與執行結果如圖七)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614CO0AhDL6ES.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614cg1czLsAFx.png
圖七:類別建立出的物件,也可以使用類別定義過的方法呢!

筆者這邊還要額外提到某樣功能 —— 儘管這可能對大部分人來說習以為常的事情。通常只要是程式碼編輯器 IDE ,對於類別建立出的物件的屬性與方法都會在讀者正在打字的過程中被顯示出來,這應該算是所謂的 Auto-Complete 的功能,TypeScript 也是不例外的。(如圖八)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614k9XbvwZTg8.png
圖八:基本上,幾乎所有的 IDE 應該都會對具有類別概念的語言,使用其建立的物件時會出現的 Auto-Complete 功能。

如果仔細看圖八,它也提供使用該物件之屬性或方法的型別推論結果(Type Inference),在上面的例子即是:(property) CPersonInfo.age: number

類別建構子 Constructor Function

如果 CPersonInfo 類別將 name 的值固定為 'Maxwell'age 固定為 20 以及 hasPet 設定為 false,這樣實在是不好用。

因此會需要一個辦法 —— 在使用 new 建構新物件時,能夠傳入一些參數設定物件的內容,例如:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614AP9SSRUUTN.png

不過讀者有沒有發現,CPersonInfo 在被呼叫的時候不是單純呼叫名稱,而是被看待成函式般地呼叫new CPersonInfo()

這個函式我們稱之為類別建構函式,又或者是類別建構子(Constructor Function)。

既然英文叫做 Constructor,剛好在 ES6 Class 定義的類別建構函式名稱也叫做 constructor,因此我們可以把剛剛的 CPersonInfo 類別的宣告裡,新增一個名為 constructor 的函式並且加入一些初始化值的邏輯。

(注意細節:是叫做類別建構函式,並不是叫類別建構方法)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614Olr5ANMvct.png

筆者簡單解說以上的程式碼。根據函式型別推論篇章提及到的重點,筆者把它貼過來:

《函式型別 X 積極註記 - Function Types》 之 重點 1. Implicit Any

大部分的情況下,只要定義任何函式,TypeScript 通常會無條件推論函式內的參數(Parameters)為 any 型別,這種現象我們稱之為 Implicit Any。

從以上重點得知 —— 由於建構子被視為是函式的一種,所以建構子裡的參數一定要被註記,不然一律被 TypeScript 視為 any,引發 Implicit any 問題。

另外,讀者發現以上的程式碼裡,我們不需要再把預設值填進成員變數的後方。那是因為在建構子函式的時候,TypeScript 早就判定:這些成員變數的值,一定會在物件剛被建構的時候被開發者提供

當然也可以使用 ES6 的預設參數(Default Parameters)將預設的值改填到建構子函式的參數裡。

https://ithelp.ithome.com.tw/upload/images/20190918/2012061444uAXjIKTm.png

筆者寫一段簡單的程式碼測試類別建構物件的效果:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614VATBqcOLu8.png

將以上的程式碼進行編譯過後再去執行之結果如圖九。

https://ithelp.ithome.com.tw/upload/images/20190918/20120614kfKT8m5fYR.png
圖九:類別建構子真的還蠻方便的~

當然,類別的函式參數的檢測,TypeScript 依然會幫助我們把關。(檢測結果如圖十)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614WADOASl9aL.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614CTBLFy4APB.png
圖十:不管是填錯型別、呼叫不存在屬性或方法都會被 TS 警告!

儘管我們已經看到建構子的用處,筆者這邊整理的重點非常重要 —— 以下這些細節會不停在討論與類別(Class)相關的篇章中重複提及。

重點 4. 類別的建構子函式 Constructor Function

類別建構子函式有以下特點:

  1. 專門進行物件初始化的動作:若宣告某類別 C,則每一次建立屬於 C 類別的物件時,最先跑的程序即是建構子函式裡的內容。而 C 本身就是那個建構子函式。
  2. 類別的宣告不一定要存在建構子函式,而建構子函式的預設值是空函式。(即 function() {})前提是,該類別沒有繼承自其他的類別(Inheritance)。
  3. 若要在宣告類別時定義建構子函式的程式內容,必須使用關鍵字 constructor 作為建構子名稱。可直接把建構子當成函式撰寫;若建構子含有參數部分,必須積極註記
  4. 建構子函式通常只用來進行初始化成員變數,強烈建議禁止塞其他的邏輯進去
  5. 若必須要在物件建構之初,執行其他的 Business Logic,則建議將這些程序進行抽象化(Abstraction)並定義為當前類別之下的方法後,在建構子函式進行呼叫的動作。(通常抽象化過後的方法,會被標記為私有 private

初入類別(Class)的讀者,看到有些重點提到繼承跟 private 這兩個詞,可能會覺得模糊。筆者會在後續部分提到建構子的注意事項!讀者目前只要知道建構子的目的就是要初始化成員變數,其餘邏輯強烈建議不要放在裡面就好,不用擔心像是第二點講到的:

類別的宣告不一定要存在建構子函式,而建構子函式的預設值是空函式。(即 function() {})前提是,該類別沒有繼承自其他的類別(Inheritance)

筆者還沒有講到繼承(Inheritance)的概念,所以讀者目前先聽過這個名詞就好了

讀者可能還有一個問題:

為何只能放初始化成員變數的邏輯而不能放其他的 Business Logic 呢?所有東西擺在一起,這樣不是很方便嗎?

建構子的目的就是只有進行初始化動作

讀者如果記得在 Day 13. 短暫提到的 SRP(Single-Responsibility Principle)的話,它明顯主張:一個類別(延伸出去即是函式、介面等等)一次只專注做一件事情 —— 將這個概念類比到建構子函式,就是告訴你:“這裡是專門初始化物件的成員變數,不是叫你順便把其他邏輯塞在一起”。

小結

今天應該算是超入門類別(Class)的語法與意義教學!不過請不熟悉類別的讀者謹記這些名詞的代表意思:

  • 成員變數 Member Variables
  • 成員方法 Member Methods
  • 建構子函式 Constructor Function

後續篇章會一直用到這些名詞,而且我們還有一大堆類別相關的功能還沒講呢!


上一篇
Day 17. 機動藍圖・複合型別 X 型別複合 - TypeScript Union & Intersection
下一篇
Day 19. 機動藍圖・存取修飾 X 藍圖規劃 - TypeScript Class Access Modifiers
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言