一般來說,我們會預期 class 裡 private 的 property 在被實體化為 object 後,無法被存取,然而最近在研究 TypeScript 才發現,原來最常見的寫法其實是可以透過其他方法存取到這個 property 的。
Table of Contents
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
關鍵字
- 在 class 內部的 property 前面加上
- Pros
- 最常見的寫法,通常也是其他 OOP 程式語言的寫法
- Cons
- 即便是實體化後的 object,也可以用 bracket notation 存取到該 object 的 private property
3.2 Hard Private
- 用法
- JavaScript 原生的寫法:hash prefix (
#
) - WeakMap
- closure
- JavaScript 原生的寫法:hash prefix (
- Pros
- 實體化後的 object 無法存取到 private property
- Cons
- 根據 compile 後的結果,可能對效能有影響
4. 參考資料
TypeScript: Documentation – Classes
Private properties – JavaScript – MDN Web Docs
如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!