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建立Shapeclass 的 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建立circleObject - 執行
move()與draw()instance method
這是完全 OOP 風格寫法,也再次證明 ECMAScript 為 multi-paradigm 語言,可完整實現 class-based 風格 OOP

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 functionsuper()要以Shape.call()實現,並將目前Circle的this傳入取代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

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()建立shapeObject,相當於 constructor function 地位直接建立 object literal 回傳,
move()為shapeObject 的 method,因為要使用this存取 property,所以要使用 function expression由於
createShape()只是一般 function,因此建立shapeObject 不必使用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()建立circleObject,相當於 constructor function 地位 - 因為
circleObject 要繼承shapeObject,以createShape()直接回傳shapeObject,並傳給Object.create()以之為 Prototype 建立新 Object - 直接在
circleObject 建立radiusproperty - 直接在
circleObject 建立drawmethod
由於沒使用 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,用法完全一樣。

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()原意就是要修改x與y,會先將shape使用...展開後再加以覆蓋
可發現 instance method 會改以在最後一個 argument 以傳第 Object 方式實現
第 7 行
let createCircle = (x, y, radius) => ({...createShape(x, y), radius })
實現 Circle 的 constructor。
extends 與 super() 會直接呼叫 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 有模組化概念

Conclusion
- 四種寫法的結果都一樣,也都建立了
circleObject,並呼叫move()與draw() - Class 寫法需使用
this、extends與super概念 - 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 最精簡易懂,行數也最少