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
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()
建立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,用法完全一樣。
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
- 四種寫法的結果都一樣,也都建立了
circle
Object,並呼叫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 最精簡易懂,行數也最少