private

TypeScript 筆記 – 那些你可能不知道的 private 特性

一般來說,我們會預期 class 裡 private 的 property 在被實體化為 object 後,無法被存取,然而最近在研究 TypeScript 才發現,原來最常見的寫法其實是可以透過其他方法存取到這個 property 的。

1. private 的特性

先來說說一般來講我們預期的 private 特性。

延伸閱讀:TypeScript 物件中的 public, private, protected, static, readonly

1.1 只能在 class 內部被存取

在 class 內部,只要在 property 前面加上 private 這個關鍵字,就會讓該 property 只能在 class 內部被存取,一但實體化後,就無法 access 這個 property:

class ProductItem {
  private cost: number
  name?: string

  constructor(cost: number, name?: string) {
    this.cost = cost
    this.name = name
  }

  private printName() {
    console.log(this.name)
  }
}

const productItem = new ProductItem(10, 'cup')
productItem.cost
// Error: Property 'cost' is private and only accessible within class 'ProductItem'.
productItem.printName()
// Error: Property 'printName' is private and only accessible within class 'ProductItem'.

即便是繼承的 class 後實體化也無法存取:

class Cup extends ProductItem {
  constructor(cost: number) {
    super(cost)
  }
}

const cup = new Cup(10)
cup.cost
// Error: Property 'cost' is private and only accessible within class 'ProductItem'.
cup.printName()
// Error: Property 'printName' is private and only accessible within class 'ProductItem'.

1.2 Cross-instance private access

某些 OOP 的程式語言允許 class 內部存取該 class 實體化後 object 的 private member,ex: Java, C#, C++, Swift, PHP,某些則不行,ex: Ruby。

TypeScript 則是允許的:

class ProductItem {
  private cost: number
  name?: string

  constructor(cost: number, name?: string) {
    this.cost = cost
    this.name = name
  }

  public printOtherInstanceCost(otherProductItem: ProductItem) {
    console.log(otherProductItem.cost)
  }
}

const cup = new ProductItem(10, 'cup')
const plate = new ProductItem(20, 'plate')
console.log(cup.printOtherInstanceCost(plate))
// 20

2. Private 的種類

在 TypeScript 中,private 還可以分成 soft private 和 hard private,最大的差別在於實體化成 object 後還能不能存取到 private member。

2.1 Soft Private

這是最常見的寫法,也是上述介紹到的寫法:在 property 或是 method 前加上 private 關鍵字。至於為什麼叫 soft private?因為這樣的寫法在實體化成 object 後,雖然無法用 dot notation 存取 object 的 property,卻可以用 bracket notation 存取到:

class ProductItem {
  private cost: number
  name?: string

  constructor(cost: number, name?: string) {
    this.cost = cost
    this.name = name
  }

  private printName() {
    console.log(this.name)
  }
}

const cup = new ProductItem(10, 'cup')

// use dot notation
console.log(cup.cost)
// Error! Property 'cost' is private and only accessible within class 'ProductItem'.

// use bracket notation
console.log(cup['cost'])
// 10
cup['printName']()
// cup

至於為什麼會有這樣的特性呢?最主要是因為 TypeScript compile 成 js 後,並不會對 private member 做額外的處理,就只是個普通的 object:

// JavaScript
class ProductItem {
    constructor(cost, name) {
        this.cost = cost;
        this.name = name;
    }
    printName() {
        console.log(this.name);
    }
}

因此要注意的是,compile 成 js 後,是連 dot notation 都可以存取 private member 的。

2.2 Hard Private

那麼如果真的不希望初始化後的 object 還能被存取到 private member 怎麼辦呢?那就必須要用原生 JavaScript 的 private 寫法。在 JavaScript 中,可以使用 hash prefix(#) 來表示該 property 是 private 的,TypeScript 也支援這種寫法:

class ProductItem {
  #cost: number
  name?: string

  constructor(cost: number, name?: string) {
    this.#cost = cost
    this.name = name
  }

  private printName() {
    console.log(this.name)
  }
}

const cup = new ProductItem(10, 'cup')

// use dot notation
console.log(cup.#cost)
// Error! Property '#cost' is not accessible outside class 'ProductItem' because it has a private identifier.

// use bracket notation
console.log(cup['#cost'])
// Error! Element implicitly has an 'any' type because expression of type '"#cost"' can't be used to index type 'ProductItem'.
console.log(cup[('#cost' as keyof ProductItem)])
// undefined

而 compile 成 JavaScript 則分為兩種版本:在 tsconfig.json 的 target 如果是 es2021 或是以下版本,會用 weakMap 來實作 private property 的部分,如果是 es2022 以上,則直接用原生 JavaScript 的 # 寫法,推測是 es2022 以後,JavaScript 才真的支援 private property。

es2021 以前:

// JavaScript
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _ProductItem_cost;
class ProductItem {
    constructor(cost, name) {
        _ProductItem_cost.set(this, void 0);
        __classPrivateFieldSet(this, _ProductItem_cost, cost, "f");
        this.name = name;
    }
    printName() {
        console.log(this.name);
    }
}
_ProductItem_cost = new WeakMap();

es2022 以後:

// JavaScript
"use strict";
class ProductItem {
    #cost;
    name;
    constructor(cost, name) {
        this.#cost = cost;
        this.name = name;
    }
    printName() {
        console.log(this.name);
    }
}

除了 WeakMap 和 JavaScript 原生的 private (hash prefix),closure 也可以用來實作 hard private。

3. 結論

3.1 Soft Private

  • 用法
    • 在 class 內部的 property 前面加上 private 關鍵字
  • Pros
    • 最常見的寫法,通常也是其他 OOP 程式語言的寫法
  • Cons
    • 即便是實體化後的 object,也可以用 bracket notation 存取到該 object 的 private property

3.2 Hard Private

  • 用法
    • JavaScript 原生的寫法:hash prefix (#)
    • WeakMap
    • closure
  • Pros
    • 實體化後的 object 無法存取到 private property
  • Cons
    • 根據 compile 後的結果,可能對效能有影響

4. 參考資料

TypeScript: Documentation – Classes
Private properties – JavaScript – MDN Web Docs

如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top