iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 19
1
Modern Web

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

Day 19. 機動藍圖・存取修飾 X 藍圖規劃 - TypeScript Class Access Modifiers

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

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

  1. TypeScript 類別(Class)的意義是什麼?
  2. TypeScript 類別跟介面(Interface)的最大差別在哪裡?
  3. 什麼是成員變數(Member Variables)、成員方法(Member Methods)以及建構子函式(Constructor Function)呢?

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

今天要講一個非常重要的東西,並且這個功能 —— 存取修飾子(Access Modifiers)是 ES6 Class 原本沒有的東西,而目前還在進行 stage-3 Proposal 階段的也就僅僅是 Private Method(未來的某個時間點會被推出)。

本身沒有 OOP 背景的讀者可能會覺得:“什麼是存取修飾子?一開篇怎麼還沒進到正文就看不懂了?”

沒關係,筆者馬上進入正文 XD

正文開始

TypeScript 類別之存取修飾子 Access Modifiers

提款機範例

大家應該都會對提款機這一個東西很熟悉吧~ 不外乎就是裡面充滿 $$$ 但是必須握有提款卡才能提領裡面的錢錢。(讀者云:“廢話!”

筆者舉一個很陽春的提款機介面大概長什麼樣子~

https://ithelp.ithome.com.tw/upload/images/20190919/20120614VrE4LAqA2Q.png

以上的程式碼,型別 TUserAccount —— 代表可以使用提款機的帳戶的靜態格式;介面 ICashMachine —— 代表該提款機的介面。

TUserAccount 除了有最簡單的帳戶與密碼欄位外,還必須紀錄金額,因此多了一個 money: number 欄位。

ICashMachine 分別有基本的存提款功能(depositwithdraw)以及使用者帳戶系統(userscurrentUsersignInsignOut)。不過筆者認為,因為 ICashMachine 介面包含這兩種不同的功能,因此也可以抽象化拆成兩個部分再組起來:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614JTbTMt2R1f.png

這樣是不是就很清楚:提款機明確分成帳戶系統與交易系統。另外,帳戶系統 AccountSystem 介面可能也可以應用在其他的系統裡面。

回歸正題,接下來筆者實際用類別去實踐 ICashMachine 這個介面定義出來的規格!

這裡筆者會先用到還沒有講到的東西 —— 類別如果要根據某介面的規格實踐出來的話,可以使用 implements 關鍵字

https://ithelp.ithome.com.tw/upload/images/20190919/20120614aE3yxReX4I.png

貼心小提示

筆者這邊短暫解釋:類別使用 implements 連結某介面(或型別)與我們在對某變數積極註記某一個型別/介面的概念很像。因此可以想成類別 implements 某介面 —— 就等同於我們在對類別進行積極註記(Annotation)的動作

Day 24. 與 Day 25. 會討論類別結合介面的推論(Inference)與註記(Annotation)機制喔!

筆者對以上的程式碼進行說明:

  • 本類別裡,每一次創建新的提款機的物件,users 欄位(或者可以稱之為 —— 成員變數 users)固定只會有三個帳戶(讀者也可以試試看用 constructor 建構子來初始化 users
  • currentUser 一開始是 undefined 的狀態
  • signIn 裡面的方法型別跟 AccountSystemsignIn 對應的型別一模ㄧ樣 —— 填入 accountpassword 後,經由簡單的 for 迴圈尋找使用者並鎖定起來
  • depositwithdraw 會先檢查有沒有使用者;如果有的話就會正常動作,沒辦法就會拋出例外

筆者以下來測試看看。(使用 TS 編譯器編譯過後與執行結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190919/20120614OfA12j1pSG.png

https://ithelp.ithome.com.tw/upload/images/20190919/20120614tbpsh7VMvp.png
圖一:完整的登入 -> 提款 -> 登出流程,執行結果很正常

儘管我們可以確定這個類別實踐出來的結果正確了,不過 ...

讀者敢用這款陽春的提款機系統嗎? XD

首先,CashMachine 類別產出來的提款機物件 -- 裡面所有的屬性與方法都可以被外部存取(Access)。其實存取這個詞筆者覺得講起來很怪,乾脆說:“裡面的屬性與方法隨時隨地都可以被檢視與呼叫!”

也就是說,光是使用 machine.usersmachine.currentUser 就可以調閱出所有的使用者資料,裡面除了基本的帳戶資訊外 —— 密碼以及個資等等都會被洩漏出去啊!

這不是我們希望看到的狀況:

於是上帝在創造類別後的第二天,創造出類別成員的存取修飾子(Member Access Modifiers)

至少不是大洪水

存取修飾子的使用與意義 Access Modifiers

筆者這裡就先下重點:

重點 1. 存取修飾子 Access Modifiers

  1. 存取修飾子總共分為三種模式:publicprivate 以及 protected
  2. 存取修飾子可以調整成員變數(Member Variables)與方法(Member Methods)在類別裡面與類別外部的使用限制
  3. 類別在宣告時,若成員變數或方法沒有被註記上存取修飾子,預設就是 public 模式
  4. 若宣告某類別 C,則裡面的成員變數 P 或成員方法 M 被註記為:
    • public 模式時:PM 可以任意在類別內外以及繼承 C 的子類別使用
    • private 模式時:PM 僅僅只能在當前類別 C 內部使用
    • protected 模式時: PM 除了當前類別 C 內部使用外,繼承 C 的子類別也可以使用
  5. 若宣告某類別 C,其中該類別有明確實踐(implements)某介面 I,則類別 C 必須實踐所有介面 I 所提供的格式 —— 而介面 I 的規格轉換成為類別 C 時 —— 成員變數與方法皆必須為 public 模式

貼心小提示

有關於類別繼承與 protected 模式將會在 Day 20. 篇章揭曉。

今天先講本篇重點 —— 存取修飾子的用途與意義

其實上面的重點已經將存取修飾子的意義講出來了:限制成員變數或方法被呼叫的權限

剛剛的 CashMachine 類別的提款機範例裡,所有的成員變數跟成員方法因為沒有出現存取修飾子的蹤跡,因此判定 —— 所有成員變數與方法都是 public 模式

也就是說,以下定義 CashMachine 類別的程式碼(每個成員變數與方法前面都加 public)與原先沒有標注 public —— 兩者行為完全沒兩樣:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614QzyBo1cRUB.png

如果想要限制使用者不能檢視成員變數的狀況,可以將剛剛的 userscurrentUser 標註 private 修飾子。(程式碼如下,被 TypeScript 檢測結果如圖二;錯誤訊息如圖三)

https://ithelp.ithome.com.tw/upload/images/20190919/20120614RyUdvtjXxe.png

https://ithelp.ithome.com.tw/upload/images/20190919/20120614Euie3TPZLf.png
圖二:將 userscurrentUser 改成 private 模式,依然被 TS 警告

https://ithelp.ithome.com.tw/upload/images/20190919/20120614cM9wP7QMNX.png
圖三:CashMachineusersprivate 模式但 ICashMachine 則是 public

儘管我們對 userscurrentUser 改了模式,但依然出現警告,通常這是初次使用 TypeScript 介面與類別會遇到的問題。因此,筆者必須另外提及一個重點:

重點 2. 介面相對於類別的意義

TypeScript 介面(Interface)定義的是功能的完整規格外,若類別對介面進行綁定的動作,裡面的規格細目代表類別成員 public 模式的成員

回過頭來,請讀者思考一件事情:

為何介面定義的規格必須是絕對綁定為 public 模式呢

其實概念很簡單,譬如說開車的時候,你操作的是方向盤、手排檔等等東西 —— 這些都是你在操縱汽車的介面

然而,你會去手動操作這些東西嗎? —— 比如說手動播速度計:此時時速為 100km/hr,亦或者翻開油箱查看現在的油量?(超猛的!)

當然是不會的!因為這些都是汽車的內部零件與細部功能互相連動,你只能操控表面的東西與看到速度計或其他資訊

這些你看得到的東西通通都屬於汽車的介面(跟 public 概念很像),你看不到的東西不是介面的範疇(跟 private 很像)。

因此可以這麼說:

interface 如果有定義包含 private 屬性的東西事實上是不合理的!(參見 StackOverflow

回過頭來,我們的提款機中的 userscurrentUser 因為被標記為 private 模式,因此 AccountSystem 介面只能有 signInsignOut 這兩個方法 —— 至於是依靠什麼方式登入登出,可以進行自由實踐。

https://ithelp.ithome.com.tw/upload/images/20190919/20120614StDcLh5S3n.png

修改完之後,回頭再看類別 CashMachine 的錯誤訊息,基本上會消失,筆者這邊就不放結果圖了。

以下是經過修改過後的完整程式碼(圖會有些長)。

https://ithelp.ithome.com.tw/upload/images/20190919/201206148Dhk6HV03k.png

注意:我們已經更換 userscurrentUserprivate 模式,並且把那兩個屬性規格從 AccountSystem 剔除掉了。

筆者測試看看以下使用 const machine = new CashMachine() 的狀況。(圖四為在 CashMachine 內部使用 userscurrentUser 的狀況;圖五則是在類別外部使用的狀況;圖六則是在類別外部使用時出現的錯誤訊息)

https://ithelp.ithome.com.tw/upload/images/20190919/201206140YCLnzPQGS.png
圖四:在類別的內部實踐出 signIn 這個方法 —— 裡面有呼叫到 userscurrentUser 都不會出現問題。

https://ithelp.ithome.com.tw/upload/images/20190919/20120614DhVT3H5N7K.png
圖五:在類別的外部,使用 machine 這個屬於類別 CashMachine 產出的物件,呼叫 currentUser 屬性就被 TypeScript 警告了。

https://ithelp.ithome.com.tw/upload/images/20190919/20120614s36vtgfnfa.png
圖六:TypeScript 很明確地跟你講,currentUserprivate 模式並且只能在 CashMachine 內部使用。

這裡應該可以看得出來 —— 藉由 private 模式將類別內部實踐出的功能隱藏起來,防止外部的開發者做出不良行為。

初始化成員變數的多種方法

另外,前一節的問題是:每一次初始化新的提款機物件,該物件都會被限制在三種帳戶的狀況。

其中,前一篇 文章介紹類別的基本宣告與用法就已經提過 —— 除了可以將成員變數定義在類別裡面、成員方法外面,我們還可以將成員變數的在建構子函式(Constructor Function)裡進行初始化動作。因此我們可以這樣做:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614gaEToSDqY4.png

但還有一種更簡潔的寫法:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614f6VACEER5X.png

這種寫法除了將 users 宣告成成員變數外,也直接把 users 設定為 private 模式 —— 也因此我們不需要再建構子函式內寫這一行:this.users = users

https://ithelp.ithome.com.tw/upload/images/20190919/20120614JkjEyzy8Rl.png
圖七:節選自某段 Angular 官方教學的程式碼,其中可以看出,該類別 HeroService 在建構子函式內直接宣告其成員變數 messageServiceprivate 模式,對應的是某 MessageService 介面(或型別)

重點 3. 建構子函式裡的參數直接宣告成員變數

若某類別 C 的宣告裡,P1P2、...、Pn 為其成員變數 —— 每個成員變數對應型別(或介面)分別為 T1T2、...、Tn

其中,P1Pn 的存取模式可為任意修飾子(publicprivate 以及 protected),則我們可以直接在 C 的建構子函式的參數內進行成員變數的宣告:

https://ithelp.ithome.com.tw/upload/images/20190919/20120614D5YTN5lbSP.png

不過必須注意的是:就算在建構子的參數裡,想要宣告 public 模式的成員變數,你必須明確註記 public 這個修飾子出來,不然 TypeScript 只能當該參數為普通參數而不是該類別的成員變數

再者,建構子裡的參數 —— 宣告的順序有差:使用 new C(/* 參數 */) 建構新物件時,填進參數的順序跟你在宣告成員變數在建構子函式的參數順序一模一樣喔!

小結

今天已經介紹完了基本的存取修飾子,明天會進到稍微困難一點的部份 —— 類別的繼承(Class Inheritance)。

畢竟這兩篇講解類別的基礎時,都提到了繼承這個字眼;再者,如果已經學會了存取修飾的概念,要進到類別的繼承其實算簡單。


上一篇
Day 18. 機動藍圖・類別宣告 X 藍圖設計 - TypeScript Class
下一篇
Day 20. 機動藍圖・類別繼承 X 延用設計 - TypeScript Class Inheritance
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
iT邦新手 2 級 ‧ 2022-09-20 16:18:31

補充一下,在看這部分時發現:

privateprotected 都只是 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

我要留言

立即登入留言