Classical Inheritance vs. Modern Inheritance

JavaScript 沒有 class 的概念,使用 new 雖然很像 class 的語法,但 constructor function 仍舊只是個 function。

  • Classical pattern:以 class 的想法模擬出繼承(inheritance)的方式
  • Modern pattern:不思考 class 的方式(應盡量選擇此 pattern)

Classical Inheritance

目的:使 Child() 所建立的 instance 可以取得 Parent() 的 property

1
2
3
4
5
6
7
8
9
10
11
12
function Parent(name) {
this.name = name || "Father";
}
Parent.prototype.getName = function () {
return this.name;
};
function Child(name) {}
// Child 繼承 Parent
inherit(Child, Parent);

#1 The Default Pattern

1
2
3
4
5
6
function inherit(C, P) {
C.prototype = new P(); // 應指向 instance 而非 function(務必加 new)
}
var kid = new Child();
kid.getName(); // "Father"
  • 缺點:同時繼承了 this 的 property 與 prototype 的 property,通常 this 的 property 是屬於 instance 專屬的,所以並不希望這些被繼承,而我們會把可 reuse 的 member 放在 prototype 中。
  • 缺點:無法透過 child 的 constructor 傳遞參數給 parent 的 method

#2 Rent a Constructor

1
2
3
4
5
6
7
function Child(name) {
Parent.apply(this, arguments); // 若有多行則可實現多重繼承
}
var kid = new Child("Son");
kid.name; // "Son"
typeof kid.getName; // "undefined"
  • 優點:解決 #1 的第二個缺點,且將 parent 中 this 的 property 複製到 child,不會有覆寫的風險
  • 缺點:prototype 的 property 都沒被繼承

#3 Rent & Set Prototype

1
2
3
4
5
6
7
8
9
10
function Child(name) {
Parent.apply(this, arguments);
}
Child.prototype = new Parent();
var kid = new Child("Son");
kid.name; // "Son"
kid.getName(); // "Son"
delete kid.name;
kid.getName(); // "Father"
  • 優點:child 繼承了 parent 自身 member 的複製,也繼承了 parent 的 prototype(即 #1 與 #2 的組合)
  • 缺點:Parent() 被呼叫了兩次,效率較差

#4 Share the Prototype

1
2
3
function inherit(C, P) {
C.prototype = P.prototype;
}
  • 優點:不會呼叫 Parent() 兩次
  • 缺點:修改 prototype 後會影響繼承的所有 object

#5 Temporary Constructor

1
2
3
4
5
function inherit(C, P) {
var F = function () {};
F.prototype = P.prototype;
C.prototype = new F();
}
  • 優點:解決了 #4 的缺點

#最終版

1
2
3
4
5
6
7
8
9
var inherit = (function () { // 以 immediate function 避免每次重複建立 temporary constructor
var F = function () {};
return function (C, P) {
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype; // superclass
C.prototype.constructor = C; // 重設 constructor pointer
}
}());

Modern Inheritance

概念:object 繼承自其他 object,即從 parent object 取得功能以建立 child object。

Prototypal Inheritance

1
2
3
4
5
6
7
8
9
10
11
12
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var parent = {
name: "Father"
};
var child = object(parent);
console.log(child.name); // "Father"

不一定要用 object literal,也可用 constructor function:

1
2
3
4
5
6
7
8
9
10
function Parent() {
this.name = "Father";
}
Parent.prototype.getName = function () {
return this.name;
};
var kid = object(new Parent()); // 只繼承 prototype 時:object(Parent.prototype)
kid.getName(); // "Father"

在 ECMAScript 5 中,不需自己實作 object(),可直接用 var child = Object.create(parent);

Inheritance by Copying Properties

此 pattern 沒有複製到 prototype,只有自身 property。

淺層複製(不檢查是否為 object 或 array,只複製到 reference)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function extend(parent, child) {
var i;
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
child[i] = parent[i];
}
}
return child;
}
var parent = {name: "Father"};
var child = extend(parent);
child.name; // "Father"

深層複製(將 object 或 array 也複製)

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
29
30
function extendDeep(parent, child) {
var i,
toStr = Object.prototype.toString,
astr = "[object Array]";
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
if (typeof parent[i] === "object") {
child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
extendDeep(parent[i], child[i]);
}
else {
child[i] = parent[i];
}
}
}
return child;
}
var parent = {list: [1, 2, 3, 4]};
var child = extendDeep(parent);
child.list.push(5);
child.list.toString(); // "1,2,3,4,5"
parent.list.toString(); // "1,2,3,4"
var child2 = extend(parent);
child2.list.push(5);
child2.list.toString(); // "1,2,3,4,5"
parent.list.toString(); // "1,2,3,4,5" (parent.list被覆寫)

Borrowing Method

只使用想用的 method,不真的繼承所有 property。使用 call()apply() 實作:

1
2
3
obj1.someFn.call(obj2, p1, p2, p3); // 即 obj1.someFn(p1, p2, p3);,且其中的 this 指向 obj2
// or
obj1.someFn.apply(obj2, [p1, p2, p3]);