ES5
中已經很常見了,後兩種方法是 ES6
中新出現的。現在我們在同一個案例上分別用這四種方法來實踐一下:
1. 在類的建構函式作用域中處理私有資料成員
我們要演示的這段程式碼是一個名為 Countdown
的類在 counter
(初始值為 counter)變成0時觸發一個名為 action
的回撥函式。其中 action
和 counter
兩個引數應被儲存為私有資料。
在這個實現方案中,我們將 action
和 counter
儲存在 constructor
這個類的環境裡面。環境是指JS引擎儲存引數和本地變數的內部資料結構,變數存在即可,無論是否進入一個新的作用域(例如通過一個函式呼叫或者一個類呼叫)。來看看程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Countdown { constructor(counter, action) { Object.assign(this, { dec() { if (counter < 1) return; counter--; if (counter === 0) { action(); } } }); } } |
然後這樣使用 Countdown:
1 2 3 4 |
> let c = new Countdown(2, () => console.log('DONE')); > c.dec(); > c.dec(); DONE |
優點:
- 私有資料非常安全;
- 私有屬性的命名不會與其他父類或子類的私有屬性命名衝突。
缺點:
- 當你需要在建構函式內把所有方法(至少那些需要用到私有資料的方法)新增到例項的時候,程式碼看起來就沒那麼優雅了;
- 作為例項方法,程式碼會浪費記憶體;如果作為原型方法,則會被共享。
關於此方法的更多內容請參考:《Speaking Javascript》的 Private Data in the Environment of a Constructor (Crockford Privacy Pattern) (建構函式環境中的私有資料)章節。
2. 通過命名約定來標記私有屬性
下面的程式碼將私有資料儲存在新增了前置下劃線命名的屬性中:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Countdown { constructor(counter, action) { this._counter = counter; this._action = action; } dec() { if (this._counter < 1) return; this._counter--; if (this._counter === 0) { this._action(); } } } |
優點:
- 程式碼比較美觀;
- 可以使用原型方法。
缺點:
- 不夠安全,只能用規範去約束使用者程式碼;
- 私有屬性的命名容易衝突。
3. 通過 WeakMaps 儲存私有資料
有一個利用 WeakMap 的小技巧,結合了方法一和方法二各自的優點:安全性和能夠使用原型方法。可以參考以下程式碼:我們利用 _counter
和 _action
兩個WeakMap來儲存私有資料。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
let _counter = new WeakMap(); let _action = new WeakMap(); class Countdown { constructor(counter, action) { _counter.set(this, counter); _action.set(this, action); } dec() { let counter = _counter.get(this); if (counter < 1) return; counter--; _counter.set(this, counter); if (counter === 0) { _action.get(this)(); } } } |
_counter
和 _action
這兩個 WeakMap 都分別指向各自的私有資料。由於 WeakMap 的設計目的在於鍵名是物件的弱引用,其所對應的物件可能會被自動回收,只要不暴露 WeakMap ,私有資料就是安全的。如果想要更加保險一點,可以將 WeakMap.prototype.get
和 WeakMap.prototype.set
儲存起來再呼叫(動態地代替方法)。這樣即使有惡意程式碼篡改了可以窺探到私有資料的方法,我們的程式碼也不會受到影響。但是,我們只保護我們的程式碼不受在其之後執行的程式碼的干擾,並不能防禦先於我們程式碼執行的程式碼。
優點:
- 可以使用原型方法;
- 比屬性命名約定更加安全;
- 私有屬性命名不會衝突。
Con:
- 程式碼不如命名約定優雅。
4. 使用Symbol作為私有屬性的鍵名
另外一個儲存私有資料的方式是用 Symbol 作為其屬性的鍵名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const _counter = Symbol('counter'); const _action = Symbol('action'); class Countdown { constructor(counter, action) { this[_counter] = counter; this[_action] = action; } dec() { if (this[_counter] < 1) return; this[_counter]--; if (this[_counter] === 0) { this[_action](); } } } |
每一個 Symbol 都是唯一的,這就是為什麼使用 Symbol 的屬性鍵名之間不會衝突的原因。並且,Symbol 某種程度上來說是隱式的,但也並不完全是:
1 2 3 4 5 6 |
let c = new Countdown(2, () => console.log('DONE')); console.log(Object.keys(c)); // [] console.log(Reflect.ownKeys(c)); // [Symbol(counter), Symbol(action)] |
優點:
- 可以使用原型方法;
- 私有屬性命名不會衝突。
缺點:
- 程式碼不如命名約定優雅;
- 不太安全:可以通過
Reflect.ownKeys()
列出一個物件所有的屬性鍵名(即使用了 Symbol)。
延伸閱讀:
- Sect. “Keeping Data Private” in “Speaking JavaScript” (covers ES5 techniques)
- Chap. “Classes” in “Exploring ES6”
- Chap. “Symbols” in “Exploring ES6”