點燈坊

新しいことを始めるのに、遅すぎる挑戰はない

深入探討 Inheritance 與 Prototype Chain

Sam Xiao's Avatar 2021-01-03

ECMAScript 2015 支援了 extends,至此 ECMAScript 能輕鬆實踐 Inheritance,但事實上也能由 Prototype Chain 實現。

Version

ECMAScript 2015

Extends

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()Shape 的 instance method

12 行

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

22 行

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

extends000

儘管使用了 extends,但可發現 draw()circle 的 Prototype Object 上,而 move()Circle.prototype 的 Prototype Object 上,因為 extends 為 synatic sugar,所以完全看不到 Prototype Chain。

Prototype Chain

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

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

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 指向將來 new 所建立的 Object。

因為使用 this,因此要使用 function expression。

第 6 行

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

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

11 行

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

建立 Circle() constructor function。

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

16 行

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

Prototype Chain 要實踐 Inheritance 關鍵在這兩行。

最直覺應該是將 shape Object 指定給 circle Object 的 __proto__ 即可實踐 Inheritance。

但目前尚未建立 Object,只能從 constructor function 下手,也就是將 Object 指定給 Circle.prototype

因此使用 Object.create() 直接以 Shape.prototype 為 Prototype 建立 Object,然後再指定給 Circle.prototype 則完成 Inheritance。

但此時因為 Object 由 Shape.prototype 建立,其 constructor property 指向 Shape(),因此要以 Circle.prototype.constructor = Circle 修正其 constructor 為 Circle(),如此 circle Object 的 __proto__ 才能藉由 circle.constructor 找到 Circle() 與其 Prototype Object。

23 行

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

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

extends001

儘管由 extends 改成 Prototype Chain,但 circle 架構完全不變,也再次證明 extends 為 Prototype Chain 的 syntatic sugar。

Conclusion

  • 兩種寫法的結果都一樣,也都建立了 circle Object,並呼叫 move()draw()
  • Class 寫法需使用 thisextendssuper 概念
  • Prototype Chain 寫法需使用到 call(),尤其以 prototype 實現 extends 比較難懂