點燈坊

學而時習之,不亦悅乎

如何實踐 Encapsulation ?

Sam Xiao's Avatar 2019-12-01

ECMAScript 無論使用 Object Literal 或者 new 建立 Object,所有的 Property 都是 Public,也就是 ECMAScript 沒有 Field 概念,而 Encapsulation 算是 OOP 最基本原則之一,這也使得使用 ECMAScript 實踐 OOP 時有些許缺憾,本文使用 Closure 實踐 Encapsulation,並探討未來 Class Field 語法。

Version

macOS Catalina 10.15.1
VS Code 1.40.1
Quokka 1.0.261
Node 13.2.0
ECMAScript 2015

Constructor Function

function Person(name, age) {
  this.name = name
  this.age = age
}

let person = new Person('Sam', 18)
person.name // ?
person.age // ?

若使用 ES5 的 constructor function 搭配 new,則 nameage 都是 public property,沒有任何 encapsulation 可言。

closure000

Class + Constructor

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

let person = new Person('Sam', 18)
person.name // ?
person.age // ?

僅管用了 ES6 的 class 語法,new 之後的 nameage 依然沒有 encapsulation。

closure001

Getter + Closure

function Person(name, age) {
  return {
    get getName() { return name },
    get getAge() { return age }
  }
}

let person = Person('Sam', 18)
person.name // ?
person.age // ?
person.getName // ?
person.getAge // ?

Closure 其實也能實作 encapsulation,透過 getter 與 closure,nameage 不能夠被直接存取,必須透過 getNamegetAge,因此獲得 encapsulation。

注意 Person 不再使用 new,只是普通 function

closure002

Arrow Function + Closure

function Person(name, age) {
  let obj = {}

  Object.defineProperty(obj, 'getName', {
    get: () => name
  })

  Object.defineProperty(obj, 'getAge', {
    get: () => age
  })

  return obj
}

let person = Person('Sam', 40)
person.name // ?
person.age // ?
person.getName // ?
person.getAge // ?

Getter 由於語法限制,無法使用更精簡的 arrow function,若你還是堅持使用 arrow function,可改用 Object.defineProperty() 定義 getName()getAge(),則 get 可搭配 arrow function。

closure003

function Person(name, age) {
  return {
    getName: () => name,
    getAge: () => age
  }
}

let person = Person('Sam', 40)
person.getName() // ?
person.getAge() // ?

若你不在乎 getNamegetAge 從 property 變成 method,則可直接使用 arrow function。

closure004

Class Fields

class Person {
  #name = ''
  #age = 0

  constructor(name, age) {
    this.#name = name
    this.#age = age
  }

  getName() {
    return this.#name
  }

  getAge() {
    return this.#age
  }
}

let person = new Person('Sam', 18)
console.log(person.getName())
console.log(person.getAge())

以上方式都是借助 closure 完成,不算是從 OOP 根本解決,ECMAScript 提出了新的 class field 語法,目前還在 stage 3,即將成為標準。

第 2 行

#name = ''
#age = 0

constructor(name, age) {
  this.#name = name
  this.#age = age
}

在 variable 前加上 # 即成為 private field。

第 10 行

getName() {
  return this.#name
}

getAge() {
  return this.#age
}

外界無法直接存取 #name#age,必須透過 getName()getAge() 才能存取。

Conclusion

  • Encapsulatio 可藉由 closure 間接達成
  • Class field 寫法目前在 Node 12 以上開始支援,也可加掛 Babel plugin 使用

Reference

Jordan Moore, Closures, Private Data, and Inheritance in JavaScript