Wendy Yuchen Sun

JavaScript 的 OOP

May 17, 2017

學習 JS 的 OOP 著實花了我好一段時間。Seriously, I tried my best. 但是可能因為實際編寫程式的經驗還不足,所以有些幽微但重要的角落,還沒體會得很完全。

所以這一篇只能說是自己對大概念的整理,那些幽微的角落就只好以待來日了。

Prototype & Delegation

為了減少重複撰寫程式碼,JS 程式設計師經常:

  1. 物件類似的片段打包成一個原型;
  2. 利用原型製作出透過原型鍊(prototype chain)與原型的新物件;
  3. 定義新物件的獨特之處。

如此,因為 JS 的 delegation mechanism,當無法找到一個物件特定名稱的資訊時,編譯器就會往上朝 prototype 找去。如此可以避免重複撰寫類似程式碼的麻煩,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// STEP1: make a prototype
const p = {
  init (x) {
    this.x = x;
  },
  getXPlus1 () {
    return this.x + 1;
  }
};

// STEP2: make new objs linking to prototype via prototype chain
// `Object.create()` is suitable for this job
let o1 = Object.create(p),
    o2 = Object.create(p);

// o1 & o2 do not own `init` or `getXPlus1` method
// but intepreter will find them in the prototype
o1.init(2);
o2.init(3);

o1.getXPlus1(); // 3
o2.getXPlus1(); // 4 

// STEP3: give objects uniqueness
o1.y = 33;
o2.getXMinus1 = function () {
  return this.x - 1;
};

Constructor

有時候,要初始話一個與原型物件有所聯結的新物件,要先寫 Object.create() 再給予物件獨特性,總共兩個步驟。我們不免問,有沒有一步到位的方法呢?

此時可以用到大多數對 JS OOP 的討論,會在一開始就介紹的 constructor,將物件初始化時設定獨特之處的工作打包起來;當要製造新的物件時,利用 new 運算元將物件的初始化。

new 運算元的作用類似如下:

  1. 製造一個新物件;
  2. 設定新物件的原型(通常是 new 後接 constructor 的 prototype property 所指向的物件);
  3. 以新物件為 context 呼叫 constructor 函式;
  4. 回傳新物件。

依此,我們可以這樣操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// STEP1: make a constructor
function Mammal (name, legs) {
  this.name = name;
  this.legs = legs;
};

// STEP2: give prototype shared state or methods
Mammal.prototype.greeting = function () {
  console.log(`Hi, I am ${this.name} and I have ${this.legs} legs!`);
};

// STEP3: make new objects
const cat = new Mammal("Wen", 4);
cat.greeting(); // logs "Hi, I am Wen and I have 4 legs!"

Overriding

當我們將一個物件與原型物件聯結在一起之後,有時候會想要在新物件上覆寫掉原型物件提供的資料。

1
2
3
4
5
6
7
8
9
10
11
12
const p = {
  x: 1,
  getANum () {
    return this.x;
  }
};

let o = Object.create(p);
// Override
o.getANum = function () {
  return this.x + 1;
};

有時候想要覆寫掉原型提供的資料,但同時需要使用到想覆寫的原型物件資料。例如前面 “Prototype & Delegation” 一節的例子裡,兩個物件的 getANum 方法都用到了 this.x 這個邏輯,我們有沒有可能利用原型本來就有的 this.x 呢?

1
2
3
o.getANum = function () {
  return p.getANum.call(this) + 1;
};

如果只知道方法的名字,但不知道原型是什麼,可以用 Object.getPrototypeOf 的方式解決:

1
2
3
o.getANum = function () {
  return Object.getPrototypeOf(this).getANum.call(this) + 1;
};

Polymorphism

多型,我自己私下如此理解,在 JS 的脈絡裡,是有不同的原型,但原型間也有所聯結。所以此時不是前面大多數討論所處理的,物件與原型物件之間的聯結、分化關係而已。

前頭筆記了兩種不同製造原型與物件的方式:Object.create() 和 constructor,那麼多型在這兩者又是如何表現出來呢?

如果是用 object.create(),那麼就是原本的原型衍生出來的物件,再作為另一個原型物件:

1
2
3
4
5
6
7
8
9
// Use `object.create()`
// 1st prototype & object linked to it
const p1 = {x: 1};
const o1 = Object.create(p1);

// 2nd prototype & object linked to it
const p2 = Object.create(p1);
p2.y = 2;
const o2 = Object.create(p2);

如果是用 constructor 的話,就比較複雜一點:要先製作出一個 constructor(連帶會產生其 prototype,記得加上希望所有與這個 prototype 鏈結物件共享的資料),接著製作第二個 constructor,在第二個 constructor 裡以 this 為 context 呼叫第一個 constructor 函式,之後將第二個 constructor 的 prototype 的 prototype 設定為第一個 constructor 的 prototype(XD),並將這個新指向 prototype 的 constructor 設定回第二個 constructor。

上面這一段寫起來好像繞口令,其實變成下面的 code 可能還更好理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Use constructor
// 1st constructor & object made from invoking it
function C1 (x) {
  this.x = x;
};
C1.prototype.getX = function () {
  return this.x;
};
const o1 = new C1(1);

// 2st constructor & object made from invoking it
function C2 (x, y) {
  C1.call(this, x);
  this.y = y;
};
C2.prototype = Object.create(C1.prototype);
C2.prototype.constructor = C2;
C2.prototype.xPlusY = function () {
  return this.x + this.y; // or this.getX() + this.y, same result
};
const o2 = new C2(3, 4);
o2.xPlusY(); // 7

ES6 Syntatic Sugar

以 constructor 製造原型與物件,要定義 constructor 又要定義 prototype,有沒有更簡單明瞭、同時支援多型或使用原型資料的語法可以幫忙呢?

ES6 提供了class 的語法,打包 constructor 與 prototype 各自的設定。例如我們原本會這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Set constructor
function Circle (radius) {
  this.radius = radius;
  Circle.circleMade++;
};

Object.defineProperty(Circle, "circlesMade", {
  get: function () {
    return !this._count ? 0:this._count;
  },
  set: function (val) {
    this._count = val;
  }
});

// Set prototype
Circle.prototype.getRadius = function () {
  return this.radius; 
};

可以改寫成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Circle {
  constructor (radius) {
    this.radius = radius;
    Circle.circleMade++;
  };

  static get circleMade () {
    return !this._count ? 0:this._count;
  };

  static set circleMade (val) {
    this._count = val;
  };

  getRadius () {
    return this.radius;
  };
};

採用多型時,可以用 extends 來建立兩組 constructor 與原型之間的鏈結;想進用原型的資料,則有 super 語法可以用:

1
2
3
4
5
class strangeCircle extends Circle {
  getRadius () {
    return super.getRadius() + 1;
  };
};

References

我只寫下對我而言比較初步但基礎、重要的思路,沒有關於 JS OOP 全部的細節。如果您對更深入的內容感興趣,可以參考以下外部聯結:

Comment is free.