點燈坊

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

你不一定需要 Class

Sam Xiao's Avatar 2021-01-20

ECMAScript 2015 支援 Class 之後,很多人認為總算達到 OOP 該有高度,事實上 ECMAScript 仍有其他方式亦可完成所有 Class 能做事情。

Version

ECMAScript 2015

Class

class Shape {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  move(x, y) {
    this.x = x
    this.y = y
  }
}

class Circle extends Shape {
  constructor(x, y, radius) {
    super(x, y)
    this.radius = radius
  }
  draw() {
    return `Drawing a Circle at ${this.x}, ${this.y}, Radius: ${this.radius}`
  }
}

let circle = new Circle(0, 0, 5)
circle.move(10, 10)
circle.draw() // ?

以 class 實踐 OOP 最經典的 Shape。

此範例在 C++ 經常出現,後來 Java 與 C# 亦常常使用此範例

第 1 行

class Shape {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  move(x, y) {
    this.x = x
    this.y = y
  }
}
  • 使用 constructor 建立 Shape class 的 constructor
  • move()Shpae 的 instance method

13 行

class Circle extends Shape {
  constructor(x, y, radius) {
    super(x, y)
    this.radius = radius
  }
  draw() {
    return `Drawing a Circle at ${this.x}, ${this.y}, Radius: ${this.radius}`
  }
}
  • 使用 extends 繼承 Shape
  • 使用 super() 呼叫 parent class 的 constructor
  • draw()Circle 的 instance method

23 行

let circle = new Circle(0, 0, 5)
circle.move(10, 10)
circle.draw() // ?
  • 使用 new 建立 circle Object
  • 執行 move()draw() instance method

這是完全 OOP 風格寫法,也再次證明 ECMAScript 為 multi-paradigm 語言,可完整實現 class-based 風格 OOP

class000

Constructor Function

function Shape(x, y) {
  this.x = x
  this.y = y
}

Shape.prototype.move = function(x, y) {
  this.x = x
  this.y = y
}

Shape.prototype.draw = function() {}

function Circle(x, y, radius) {
  Shape.call(this, x, y)
  this.radius = radius
}

Circle.prototype = Object.create(Shape.prototype)
Circle.prototype.constructor = Circle

Circle.prototype.draw = function() {
  return `Drawing a Circle at ${this.x}, ${this.y}, Radius: ${this.radius}`
}

let circle = new Circle(0, 0, 5)
circle.move(10, 10)
circle.draw() // ?

ES6 雖然提供 class 寫法,但骨子仍是 Prototype,class 只能算 syntatic sugar。

第 1 行

function Shape(x, y) {
  this.x = x
  this.y = y
}

建立 Shape() constructor function,因為會使用 this,因此要使用 function expression。

第 6 行

Shape.prototype.move = function(x, y) {
  this.x = x
  this.y = y
}

prototype 上建立 move() instance method 以節省記憶體。

11 行

Shape.prototype.draw = function() {}

prototype 上建立 draw() instance method。

13 行

function Circle(x, y, radius) {
  Shape.call(this, x, y)
  this.radius = radius
}
  • 建立 Circle() constructor function

  • super() 要以 Shape.call() 實現,並將目前 Circlethis 傳入取代 Shape() 本身的 this

18 行

Circle.prototype = Object.create(Shape.prototype)
Circle.prototype.constructor = Circle
  • 使用 Object.create()Shape.prototype 為 Prototype 建立 Object,並將 Circle.prototype 指向該 Object 實踐 ES6 的 extends
  • 由於 Circle.prototype 指向以 Shape.prototype 所建立的 Object,目前 constructor function 為 Shape() 是錯的,要改指向 Circle() 修正

這兩行也是以 constructor function 實現 OOP 繼承最難理解的兩行

25 行

let circle = new Circle(0, 0, 5)
circle.move(10, 10)
circle.draw() // ?

一樣使用 new 建立 circle Object,用法完全一樣。

可發現 Prototype 能完全實踐 class 所有功能,再次證明 class 只是 syntatic sugar

class001

Normal Function

let createShape = (x, y) => {
  let move = function(x, y) {
    this.x = x
    this.y = y
  }

  return { x, y, move }
}

let createCircle = (x, y, radius) => {
  let circle = Object.create(createShape(x, y))
  circle.radius = radius
  circle.draw = function() {
    return `Drawing a Circle at ${this.x}, ${this.y}, Radius: ${this.radius}`
  }

  return circle
}

let circle = createCircle(0, 0, 5)
circle.move(10, 10)
circle.draw() // ?

使用 constructor function 雖然能實現 OOP,但它必經只是 function,搭配 Prototype 時需搭配 prototype property 不是很直覺,其實有更簡單的方式。

既然 constructor function 目的是要建立 Object,其實可直接使用一般 function 回傳 Object 即可,也不必使用 new

第 1 行

let createShape = (x, y) => {
  let move = function(x, y) {
    this.x = x
    this.y = y
  }

  return { x, y, move }
}
  • createShape() 建立 shape Object,相當於 constructor function 地位

  • 直接建立 object literal 回傳,move()shape Object 的 method,因為要使用 this 存取 property,所以要使用 function expression

  • 由於 createShape() 只是一般 function,因此建立 shape Object 不必使用 new

10 行

let createCircle = (x, y, radius) => {
  let circle = Object.create(createShape(x, y))
  circle.radius = radius
  circle.draw = function() {
    return `Drawing a Circle at ${this.x}, ${this.y}, Radius: ${this.radius}`
  }

  return circle
}
  • createCircle() 建立 circle Object,相當於 constructor function 地位
  • 因為 circle Object 要繼承 shape Object,以 createShape() 直接回傳 shape Object,並傳給 Object.create() 以之為 Prototype 建立新 Object
  • 直接在 circle Object 建立 radius property
  • 直接在 circle Object 建立 draw method

由於沒使用 constructor function,直接從 Object 下手使用 Prototype,整個過程非常直覺,可發現 Prototype 被誤解主要是因為使用 constructor function 與 new,只要使用一般 function 與 Object.create(),其實 Prototype 並沒有這麼難懂

21 行

let circle = createCircle(0, 0, 5)
circle.move(10, 10)
circle.draw() // ?

一樣使用 new 建立 circle Object,用法完全一樣。

class003

Function Pipeline

import { pipe } from 'ramda'

let createShape = (x, y) => ({ x, y })

let move = (x, y) => shape => ({ ...shape, x, y })

let createCircle = (x, y, radius) => ({...createShape(x, y), radius })

let draw = circle => `Drawing a Circle at ${circle.x}, ${circle.y}, Radius: ${circle.radius}`

pipe(
  createCircle,
  move(10,10),
  draw
)(0, 0, 5) // ?

既然結果是 circle Object,其實可以完全使用 function 與 Object 的方式實踐,完全不需要 class 與 Prototype。

第 3 行

let createShape = (x, y) => ({ x, y })

實現 Shape 的 constructor,完全沒使用到 this,直接回傳 Object。

因為沒使用到 this,可安心使用 arrow function

第 5 行

let move = (x, y) => shape => ({ ...shape, x, y })
  • 實現 move() instance method,會將 Object 以 currying 放到最後一個 argument 方便 Function Pipeline
  • move() 原意就是要修改 xy,會先將 shape 使用 ... 展開後再加以覆蓋

可發現 instance method 會改以在最後一個 argument 以傳第 Object 方式實現

第 7 行

let createCircle = (x, y, radius) => ({...createShape(x, y), radius })

實現 Circle 的 constructor。

extendssuper() 會直接呼叫 createShape() 並以 ... 展開實現。

第 9 行

let draw = circle => `Drawing a Circle at ${circle.x}, ${circle.y}, Radius: ${circle.radius}`

實現 draw() instance method,在最後一個 argument 傳入 circle Object。

11 行

pipe(
  createCircle,
  move(10,10),
  draw
)(0, 0, 5) // ?

我們將 Object 都以 currying 放在最後一個 argument,就是為了 Function Pipeline。

createCircle() 會傳回 circle Object,順勢傳到 move(),而 move() 又會回傳 circle Object,又可順勢傳到 draw(),整個過程以 pipe() 加以組合。

實務上會將 Shape 相關 function 重構到一個 module,Circle 相關 function 重構到另一個 module,如此可如 class 有模組化概念

class002

Conclusion

  • 四種寫法的結果都一樣,也都建立了 circle Object,並呼叫 move()draw()
  • Class 寫法需使用 thisextendssuper 概念
  • Constructor function 寫法需使用到 call(),尤其以 constructor function 的 prototype 實現 extends 比較難理解
  • Normal function 直接以 Object.create() 實現 extends,非常直覺
  • Function Pipeline 寫法會將 Object 以 currying 放到 function 最後一個 argument,並以傳遞 Object 方式維持 Object 為 Immutable,且實務上會將相關 function 重構到 module,最後以 pipe() 組合整個流程
  • 四種寫法可發現 Function Pipeline 最精簡易懂,行數也最少

Reference

LUKE知る, You don’t need classes