閱讀本篇文章前,仔細想想看
- TypeScript 類別(Class)的意義是什麼?
- TypeScript 類別跟介面(Interface)的最大差別在哪裡?
- 什麼是成員變數(Member Variables)、成員方法(Member Methods)以及建構子函式(Constructor Function)呢?
如果還沒理解完畢的話,可以先翻看前一篇文章喔!
今天要講一個非常重要的東西,並且這個功能 —— 存取修飾子(Access Modifiers)是 ES6 Class 原本沒有的東西,而目前還在進行 stage-3
Proposal 階段的也就僅僅是 Private Method(未來的某個時間點會被推出)。
本身沒有 OOP 背景的讀者可能會覺得:“什麼是存取修飾子?一開篇怎麼還沒進到正文就看不懂了?”
沒關係,筆者馬上進入正文 XD
正文開始!
大家應該都會對提款機這一個東西很熟悉吧~ 不外乎就是裡面充滿 $$$ 但是必須握有提款卡才能提領裡面的錢錢。(讀者云:“廢話!”)
筆者舉一個很陽春的提款機介面大概長什麼樣子~
以上的程式碼,型別 TUserAccount
—— 代表可以使用提款機的帳戶的靜態格式;介面 ICashMachine
—— 代表該提款機的介面。
TUserAccount
除了有最簡單的帳戶與密碼欄位外,還必須紀錄金額,因此多了一個 money: number
欄位。
ICashMachine
分別有基本的存提款功能(deposit
與 withdraw
)以及使用者帳戶系統(users
、currentUser
、signIn
與 signOut
)。不過筆者認為,因為 ICashMachine
介面包含這兩種不同的功能,因此也可以抽象化拆成兩個部分再組起來:
這樣是不是就很清楚:提款機明確分成帳戶系統與交易系統。另外,帳戶系統 AccountSystem
介面可能也可以應用在其他的系統裡面。
回歸正題,接下來筆者實際用類別去實踐 ICashMachine
這個介面定義出來的規格!
這裡筆者會先用到還沒有講到的東西 —— 類別如果要根據某介面的規格實踐出來的話,可以使用 implements
關鍵字:
貼心小提示
筆者這邊短暫解釋:類別使用
implements
連結某介面(或型別)與我們在對某變數積極註記某一個型別/介面的概念很像。因此可以想成類別implements
某介面 —— 就等同於我們在對類別進行積極註記(Annotation)的動作!Day 24. 與 Day 25. 會討論類別結合介面的推論(Inference)與註記(Annotation)機制喔!
筆者對以上的程式碼進行說明:
users
欄位(或者可以稱之為 —— 成員變數 users
)固定只會有三個帳戶(讀者也可以試試看用 constructor
建構子來初始化 users
)currentUser
一開始是 undefined
的狀態signIn
裡面的方法型別跟 AccountSystem
的 signIn
對應的型別一模ㄧ樣 —— 填入 account
與 password
後,經由簡單的 for
迴圈尋找使用者並鎖定起來deposit
和 withdraw
會先檢查有沒有使用者;如果有的話就會正常動作,沒辦法就會拋出例外筆者以下來測試看看。(使用 TS 編譯器編譯過後與執行結果如圖一)
圖一:完整的登入 -> 提款 -> 登出流程,執行結果很正常
儘管我們可以確定這個類別實踐出來的結果正確了,不過 ...
讀者敢用這款陽春的提款機系統嗎? XD
首先,CashMachine
類別產出來的提款機物件 -- 裡面所有的屬性與方法都可以被外部存取(Access)。其實存取這個詞筆者覺得講起來很怪,乾脆說:“裡面的屬性與方法隨時隨地都可以被檢視與呼叫!”
也就是說,光是使用 machine.users
或 machine.currentUser
就可以調閱出所有的使用者資料,裡面除了基本的帳戶資訊外 —— 密碼以及個資等等都會被洩漏出去啊!
這不是我們希望看到的狀況:
於是上帝在創造類別後的第二天,創造出類別成員的存取修飾子(Member Access Modifiers)
(至少不是大洪水)
筆者這裡就先下重點:
重點 1. 存取修飾子 Access Modifiers
- 存取修飾子總共分為三種模式:
public
、private
以及protected
- 存取修飾子可以調整成員變數(Member Variables)與方法(Member Methods)在類別裡面與類別外部的使用限制。
- 類別在宣告時,若成員變數或方法沒有被註記上存取修飾子,預設就是
public
模式。- 若宣告某類別
C
,則裡面的成員變數P
或成員方法M
被註記為:
public
模式時:P
與M
可以任意在類別內外以及繼承C
的子類別使用private
模式時:P
與M
僅僅只能在當前類別C
內部使用protected
模式時:P
與M
除了當前類別C
內部使用外,繼承C
的子類別也可以使用- 若宣告某類別
C
,其中該類別有明確實踐(implements
)某介面I
,則類別C
必須實踐所有介面I
所提供的格式 —— 而介面I
的規格轉換成為類別C
時 —— 成員變數與方法皆必須為public
模式
貼心小提示
有關於類別繼承與
protected
模式將會在 Day 20. 篇章揭曉。
今天先講本篇重點 —— 存取修飾子的用途與意義。
其實上面的重點已經將存取修飾子的意義講出來了:限制成員變數或方法被呼叫的權限。
剛剛的 CashMachine
類別的提款機範例裡,所有的成員變數跟成員方法因為沒有出現存取修飾子的蹤跡,因此判定 —— 所有成員變數與方法都是 public
模式!
也就是說,以下定義 CashMachine
類別的程式碼(每個成員變數與方法前面都加 public
)與原先沒有標注 public
—— 兩者行為完全沒兩樣:
如果想要限制使用者不能檢視成員變數的狀況,可以將剛剛的 users
與 currentUser
標註 private
修飾子。(程式碼如下,被 TypeScript 檢測結果如圖二;錯誤訊息如圖三)
圖二:將 users
與 currentUser
改成 private
模式,依然被 TS 警告
圖三:CashMachine
的 users
是 private
模式但 ICashMachine
則是 public
儘管我們對 users
與 currentUser
改了模式,但依然出現警告,通常這是初次使用 TypeScript 介面與類別會遇到的問題。因此,筆者必須另外提及一個重點:
重點 2. 介面相對於類別的意義
TypeScript 介面(Interface)定義的是功能的完整規格外,若類別對介面進行綁定的動作,裡面的規格細目代表類別成員
public
模式的成員。
回過頭來,請讀者思考一件事情:
為何介面定義的規格必須是絕對綁定為
public
模式呢?
其實概念很簡單,譬如說開車的時候,你操作的是方向盤、手排檔等等東西 —— 這些都是你在操縱汽車的介面。
然而,你會去手動操作這些東西嗎? —— 比如說手動播速度計:此時時速為 100km/hr,亦或者翻開油箱查看現在的油量?(超猛的!)
當然是不會的!因為這些都是汽車的內部零件與細部功能互相連動,你只能操控表面的東西與看到速度計或其他資訊。
這些你看得到的東西通通都屬於汽車的介面(跟 public
概念很像),你看不到的東西不是介面的範疇(跟 private
很像)。
因此可以這麼說:
interface
如果有定義包含private
屬性的東西事實上是不合理的!(參見 StackOverflow)
回過頭來,我們的提款機中的 users
與 currentUser
因為被標記為 private
模式,因此 AccountSystem
介面只能有 signIn
與 signOut
這兩個方法 —— 至於是依靠什麼方式登入登出,可以進行自由實踐。
修改完之後,回頭再看類別 CashMachine
的錯誤訊息,基本上會消失,筆者這邊就不放結果圖了。
以下是經過修改過後的完整程式碼(圖會有些長)。
注意:我們已經更換 users
與 currentUser
為 private
模式,並且把那兩個屬性規格從 AccountSystem
剔除掉了。
筆者測試看看以下使用 const machine = new CashMachine()
的狀況。(圖四為在 CashMachine
內部使用 users
與 currentUser
的狀況;圖五則是在類別外部使用的狀況;圖六則是在類別外部使用時出現的錯誤訊息)
圖四:在類別的內部實踐出 signIn
這個方法 —— 裡面有呼叫到 users
與 currentUser
都不會出現問題。
圖五:在類別的外部,使用 machine
這個屬於類別 CashMachine
產出的物件,呼叫 currentUser
屬性就被 TypeScript 警告了。
圖六:TypeScript 很明確地跟你講,currentUser
是 private
模式並且只能在 CashMachine
內部使用。
這裡應該可以看得出來 —— 藉由 private
模式將類別內部實踐出的功能隱藏起來,防止外部的開發者做出不良行為。
另外,前一節的問題是:每一次初始化新的提款機物件,該物件都會被限制在三種帳戶的狀況。
其中,前一篇 文章介紹類別的基本宣告與用法就已經提過 —— 除了可以將成員變數定義在類別裡面、成員方法外面,我們還可以將成員變數的值在建構子函式(Constructor Function)裡進行初始化動作。因此我們可以這樣做:
但還有一種更簡潔的寫法:
這種寫法除了將 users
宣告成成員變數外,也直接把 users
設定為 private
模式 —— 也因此我們不需要再建構子函式內寫這一行:this.users = users
。
圖七:節選自某段 Angular 官方教學的程式碼,其中可以看出,該類別 HeroService
在建構子函式內直接宣告其成員變數 messageService
為 private
模式,對應的是某 MessageService
介面(或型別)
重點 3. 建構子函式裡的參數直接宣告成員變數
若某類別
C
的宣告裡,P1
、P2
、...、Pn
為其成員變數 —— 每個成員變數對應型別(或介面)分別為T1
、T2
、...、Tn
。其中,
P1
到Pn
的存取模式可為任意修飾子(public
、private
以及protected
),則我們可以直接在C
的建構子函式的參數內進行成員變數的宣告:
不過必須注意的是:就算在建構子的參數裡,想要宣告 public
模式的成員變數,你必須明確註記 public
這個修飾子出來,不然 TypeScript 只能當該參數為普通參數而不是該類別的成員變數。
再者,建構子裡的參數 —— 宣告的順序有差:使用 new C(/* 參數 */)
建構新物件時,填進參數的順序跟你在宣告成員變數在建構子函式的參數順序一模一樣喔!
今天已經介紹完了基本的存取修飾子,明天會進到稍微困難一點的部份 —— 類別的繼承(Class Inheritance)。
畢竟這兩篇講解類別的基礎時,都提到了繼承這個字眼;再者,如果已經學會了存取修飾的概念,要進到類別的繼承其實算簡單。
補充一下,在看這部分時發現:
private
& protected
都只是 TS 在編譯時做檢查,依然可以被從外部繞過存取。
使用 js class #屬性
才能避免被外部存取。
class C1 {
protected a: string
public b: string
private c: string
#d: string
constructor(a: string, b: string, c: string, d: string) {
this.a = a
this.b = b
this.c = c
this.#d = d
}
}
const x = new C1('a', 'b', 'c', 'd')
console.log(x.a) // error
console.log(x.b) // b
console.log(x.c) // error
console.log(x.#d) // error
const y = JSON.parse(JSON.stringify(x))
console.log(y.a) // a
console.log(y.b) // b
console.log(y.c) // c
console.log(y.#d) // error