點燈坊

失くすものさえない今が強くなるチャンスよ

ECMScript 實現 Encapsulation

Sam Xiao's Avatar 2020-11-14

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

Version

ECMAScript 2020

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 with 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

Value Object Pattern

function Person(name, age) {
  _name = name
  _age = age
  
  return {
    get name() { return _name },
    get age() { return _age }
  }
}

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

可透過 value object pattern 以 closure 實作 encapsulation,_name_age 不能夠被直接存取,必須透過 nameage getter,因此獲得 encapsulation。

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

closure002

Arrow Function + Closure

function Person(name, age) {
  _name = name
  _age = age
  
  let obj = {}
  
  Object.defineProperty(obj, 'name', {
    get: () => _name
  })

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

  return obj
}

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

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

closure003

function Person(name, age) {
  _name = name
  _age = age

  return {
    getName: () => _name,
    getAge: () => _age
  }
}

let person = Person('Sam', 18)
person._name // ?
person._age // ?
person.getName() // ?
person.getAge() // ?

若你不在乎 nameage 從 property 變成 getName()getAge() method,則可使用 arrow function。

closure004

IIFE + Closure

let person = ((name, age) => {
  _name = name
  _age = age

  return {
    getName: () => _name,
    getAge: () => _age
  }
})('Sam', 18)

person._name // ?
person._age // ?
person.getName() // ?
person.getAge() // ?

也可改用 IIFE 直接回傳 Object,連 Person() 也一併省略。

closure006

Private Class Fields

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

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

  get name() {
    return this.#name
  }

  get age() {
    return this.#age
  }
}

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

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

第 2 行

#name = ''
#age = 0

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

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

第 10 行

get name() {
  return this.#name
}

get age() {
  return this.#age
}

外界無法直接存取 #name#age,必須透過 nameage getter 才能存取。

closure005

Conclusion

  • Encapsulation 可藉由 value object pattern 與 closure 間接達成
  • IIFE 可直接回傳 Object,連 function 都省略
  • Private class field 目前在 Node 12 以上開始支援,也可加掛 Babel plugin 使用

Reference

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