
很不友善的 JavaScript 物件繼承寫法
最近一年以來又開始大量JavaScript的coding,距離從前做web已經過了十年了。離開學校之後,大部分的時間都在從事Windows Application開發。為了重拾兒時開發web的記憶,看了許多新的技術,不過最讓我覺得驚奇的是 JavaScript 的 OO 語法還是一樣的不友善啊。十年前做Web的時候只需要搞定 IE6 就可以了,現在就還要考慮IE6 / IE7 / IE8 / IE9 / IE10 / Chrome / FireFox/ Opera/ Mobile Browser 。現在的Web Developer真是太辛苦了。
當時最主要的JavaScript參考網站是 DynamicDrive(現在居然還活著耶),DynamicDrive居然還保留著那時候最紅的term:Dynamic HTML。當時書店很多書都在講 DHTML,現在已經都是 jQuery/Ajax的天下了。(不由自主的懷舊起來 XD) 在當時,我們就已經大量使用 iframe 來達到現在的 Ajax 想做到的事情,不過現在當然還是Ajax比較有效率。(雖說目前有些時候還是不得已會需要用 iframe,比如說 Cross-Site Script 的問題)
Prototype Inheritance
再看過了一些近代的JavaScript寫法之後,實在還是覺得JavaScript的OO非常的特別,很值得好好研究一下。所以我再回頭看了一下JS的Object的原理。
一般的JavaScript的Class定義的方式是這樣:
1
2
3
4
5
| function Shape() // constructor of Shape class
{
this.type = 'shape';
this.area = 0;
} |
這樣一定義之後,就會有一個 Shape 的 Function Object(阿,對!JS的Function本身是一個Object),Shape這個Function Object本身會帶有一個 prototype 屬性。這個prototype屬性就是JavaScript OO寫法的重點,所有在 prototype 中的屬性都會被 new 出來的 Object Instance 繼承(以Reference的形式)。
所以,如果要定義 Shape Class 的 instance method,就直接新增一個 function property 給 Shape 的 prototype(這個時候的 prototype 也是一個 Object)。
1
2
3
4
| Shape.prototype.getArea = function() // instance method of Class Shape
{
return this.area;
} |
這時候如果create兩個 Shape 的 instance:
1
2
| var rect = new Shape();
var triangle = new Shape(); |

rect 以及 triangle兩個 object 本身都有自己各自一份 type 以及 area 屬性,至於 getArea 則會 refer 到 Shape Class 的 prototype object 裡面的 getArea。 就是這個特性延伸出後面的JavaScript物件繼承方法。
關於這個特性,另外還有兩個特別需要注意的地方:
- 第一個是關於instance method的定義以及記憶體使用量的問題
以上的範例可以改寫下面的code,執行結果會是一模一樣的。
1
2
3
4
5
6
7
8
| function Shape() // constructor of Shape class
{
this.type = 'shape';
this.area = 0;
this.getArea = funtion(){
return this.area;
}
} |
唯一的差異是,用這種寫法的話,你create的object instance越多,耗費的記憶體就越多(相對於第一種寫法)。

如上圖,每個 instance 自己本身會有一個 getArea function 的實體,而前一種寫法則是會只有一份實體,所有呼叫都會用到 prototype 下的那份實體。所以如果你 define 的 class 會被大量生成的話可能還是要考量一下這一點。
- 第二個是name hiding 的問題
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function Shape() // constructor of Shape class
{
this.type = 'shape';
this.area = 10;
this.getArea = function(){ // this definition would hide
// the one with the same name in Shape.prototype
console.log('instance');
return this.area;
};
}
Shape.prototype.getArea = function()
{
console.log('prototype');
return this.area;
} |
當呼叫某個物件下的function或是讀取某個變數的時候JavaScript的搜尋規則是先看自己本身的屬性有沒有符合要存取的 function 或是 變數名稱,如果沒有的話才會再往 prototype 去找,所以像上面的這種寫法,function Shape 裡面的 getArea 就會直接蓋掉 prototype 下的那個 getArea。
物件繼承的寫法
所以一般最原始的JavaScript的繼承寫法,假設我們把Shape當做base class,Rectangle是繼承自Shape的sub-class,那個程式碼應該會是這樣:
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
31
32
| function Shape(type) // Base Shape class
{
this.type = type;
this.w = 0;
this.h = 0;
}
Shape.prototype.getArea = function()
{
return this.area;
}
function Rectangle(w, h) // The derived class inheriting Shape
{
this.superclass('rectangle');
this.w = w;
this.h = h;
this.area = w * h;
}
Rectangle.prototype = new Shape(); // Set the derived class prototype
// object as the parent class
// object istance
// 在建立與parent的連結之後可以新增 Rectangle
// 自己本身的 method 或是 其他屬性
Rectangle.prototype.getWidth() = function(){
return this.w;
}
Rectangle.prototype.superclass = Shape; // Not necessary
// just for constructor chaining
Rectangle.prototype.constructor = Rectangle; // 根據實際測試,這可能不需要! |
其中最重要的一個步驟就是 create 一個 Shape object instance 然後 assign 給 Rectangle 的 prototype。
Rectangle.prototype = new Shape();
就如前面所說,function object一定會有一個prototype object,而 JavaScript 的物件繼承就來自於將每個 subclass 的 prototype 串接到 super class 的 object instance之後,而每個function呼叫或讀取屬性的時候會先往 prototype 裡面找,這麼一來就達成了物件繼承的基本樣子了。
Rectangle.prototype.constructor = Rectangle;
至於上面這一行,是為了把 constructor 修正為目前 class 的 constructor,大部分講到JavaSCript OO的書都會提到要做這件事情。但是我拜讀了大師Douglas Crockford的Classical Inheritance in JavaScript文章之後,發現他沒做這個動作。於是我稍微測試了一下,看來的確是可以不用。
Constructor Chaining
Rectangle.prototype.superclass = Shape;
一般的OO,通常會在sub-class的constructor自動去呼叫superclass的constructor,但是JavaScript沒有幫你做這件事情。不過由於這個動作非常的必須且重要,所以我們就直接把 superclass的constructor給keep下來成一個屬性,這樣我就可以去呼叫他了。一般書上都沒有提到這種chaining的寫法會有一個問題,就是繼承的深度大於兩層,那這個方法就完了。由於前面提到的name hiding的問題,所以如果你這時候有一個Square class去繼承自Rectangle,那這個時候的prototype的sperclass就會變成是Rectangle。那當從Rectangle的constructor去call this.superclass的時候就會呼叫到自己然後進入無窮遞迴的地獄裡!
解決方法大概就是最原始的寫法最安全:
Shape.call(this, 'rectangle');

看完了原理之後,我們都知道程式工人進步的動力來源是抄襲研究有名的source code,我們就來看看幾個有名的JavaScript函式庫是如何包裝class繼承這一塊吧。
Douglas Crockford’s
Classical Inheritance in JavaScript
第一個一定要先來看一下大師的寫法,非常的簡潔不好懂,只有短短幾行,但是該做的事情都做完了,而且對於繼承體系中向上層呼叫的事情也包裝的很巧妙:
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
31
| Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
};
Function.method('inherits', function (parent) {
var d = {}, p = (this.prototype = new parent());
this.method('uber', function uber(name) {
if (!(name in d)) {
d[name] = 0;
}
var f, r, t = d[name], v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d[name] += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d[name] -= 1;
return r;
});
return this;
}); |
我們用一個深度為三層的例子來解釋這段很巧妙的包裝方法:
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
31
32
33
34
35
36
37
| // Base X
function X(value){
this.init(value);
}
X.method('init', function(value){
this.value = value;
});
X.method('getValue', function(){
return this.value + ' from X';
});
// Y derived from X
function Y(value){
this.uber('init', value);
}
Y.inherits(X);
Y.method('getValue', function(){
return this.uber('getValue') + ' from Y';
});
Y.method('getValue2', function(){
return 'getValue2 from Y';
});
// Z derived from Y
function Z(value){
this.uber('init', value);
}
Z.inherits(Y);
Z.method('getValue', function(){
this.uber('getValue2');
return this.uber('getValue') + ' from Z';
});
var z = new Z('cool code');
console.log(z.getValue()); |
上面這段code的輸出結果是:
cool code from X from Y from Z
來拆解一下,首先針對Function這個object擴充了一個method的函式用來定義之後的class instance,之後就可以不用再寫很長的 function.prototype.method。並且後面也直接利用這個function再針對Function擴充了一個inherits函式。inherits的部分其實跟這樣寫是一樣的:
Function.prototype.inherits = function (parent) { ... };
inherits的實作第一部份很單純,就是create一個superclass的object instance塞給目前的prototype。
p = (this.prototype = new parent());
第二個部分,uber的實作方式很有趣,之前提到的name hiding的問題,透過uber你就可以往superclass呼叫到同名的函式,大概可以想像成是一種 super 語法的實作。先把它拆解成兩種cases來看:
- 第一種case是單純的呼叫superclass的某一個function (getValue2),而這個function不會再繼續在呼叫uber。由於一開始 d 是空物件(行7,所以d['getValue2']會等於0,於是行13的if判斷會走到else,之後由於name hiding的關係,行21會被evaluate為true,於是拿到parent的getValue2。之後由於這種case很單純,所以行25、27在這裡沒有意義,可以直接忽略,之後getValue2就會在行26得到結果之後回傳。
- 第二種case是call到superclass的 getValue 之後,還會繼續往上呼叫 getValue的 chaining call,一開始當call Z::getValue的時候,走的路跟第一種case一樣,但是這時候行25跟27不能忽略,於是行25會設定 d['getValue'] = 1,之後 f = Y::getValue,行26會執行Y::getValue,由於Y的getValue還會繼續call uber去呼叫X的getValue,所以會產生一個遞迴呼叫,這時候行13的t=1,所以行14~行17的while loop會往上走一層到然後行18拿到的f就是 X::getValue,之後執行完行26之後d['getValue']就變成1,在回到Y的call stack,於是d['getValue']變成 0,最後結果回傳到行33作為console.log的參數。這一段幾乎沒什麼人解釋,Crockford的網頁裡面沒有解釋,John Resig(jQuery作者)的Pro JavaScript裡面有提到這段code,也沒有多做解釋。不過我覺得這段code一開始看其實會不太知道在做什麼,所以特此解釋並做下筆記
看完之後,對於大師就是佩服啊,幾行code就搞定這個麻煩事。
Prototype.js
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| var Class = (function() {
function subclass() {};
function create() {
var parent = null, properties = $A(arguments);
if (Object.isFunction(properties[0]))
parent = properties.shift();
function klass() {
this.initialize.apply(this, arguments);
}
Object.extend(klass, Class.Methods);
klass.superclass = parent;
klass.subclasses = [];
if (parent) {
subclass.prototype = parent.prototype;
klass.prototype = new subclass;
parent.subclasses.push(klass);
}
for (var i = 0; i < properties.length; i++)
klass.addMethods(properties[i]);
if (!klass.prototype.initialize)
klass.prototype.initialize = Prototype.emptyFunction;
klass.prototype.constructor = klass;
return klass;
}
function addMethods(source) {
var ancestor = this.superclass && this.superclass.prototype;
var properties = Object.keys(source);
if (!Object.keys({ toString: true }).length) {
if (source.toString != Object.prototype.toString)
properties.push("toString");
if (source.valueOf != Object.prototype.valueOf)
properties.push("valueOf");
}
for (var i = 0, length = properties.length; i < length; i++) {
var property = properties[i], value = source[property];
if (ancestor && Object.isFunction(value) &&
value.argumentNames().first() == "$super") {
var method = value;
value = (function(m) {
return function() { return ancestor[m].apply(this, arguments); };
})(property).wrap(method);
value.valueOf = method.valueOf.bind(method);
value.toString = method.toString.bind(method);
}
this.prototype[property] = value;
}
return this;
}
return {
create: create,
Methods: {
addMethods: addMethods
}
};
})(); |
Prototype的class inheritance在ㄧ開始有註解是源自Alex Arnell’s inheritance implementation。這一長串的Class的邏輯在仔細的追蹤之後,我會覺得臻的有點奇妙,就整段的重點在於addMethods裡面在處理有宣告$super為參數的function,目的是為了達到繼承體系中往上串接的能力(其實就是 Crockford的uber想做的事情),不過寫得太過複雜,包裝方式有很高的overhead。
行8到行28基本上就是先建立一個空的klass主體,接著以Object.extend將Class.methods擴充至klass。Object.extend做的事情基本上只是把第二個參數中的所以屬性複製一份給第一個參數:
function extend(destination, source) {
for (var property in source)
destination[property] = source[property];
return destination;
}
行16到20,依照基本原則建立一個superclass的object instance作為目前klass的prototype,之後有趣的是在行28很規矩的依照原則做了constructor的調整。基本上這樣已經完成了基本的繼承架構。不過在這邊的行32到59這一個addMethods做了不少事情需要特別解釋一下。
在行36到41這一段,實在令人百思不得其解,一開始的if條件就永遠不可能為真,所以這一段code放在這裡的意義是?小弟資質駑鈍,希望可以有人為我解答一下。
行45到54這一個部分,一開始先把function toString之後再用regular expression檢查第一個參數是不是$super,是的話就會進入一個複雜的過程:
行48到50 – 建立一個匿名函式,並透過closure技巧將property帶入至m,比如說如果有一個Say function的話,那這個匿名函式就隱性的變成 function() { return ancestor[“Say"].apply(this, arguments); };。
行50 – 對上面帶出的匿名函式做一次wrap,於是就作出一個匿名函式帶有parent作為第一個參數:
function wrap(wrapper) { // wrapper 是目前的函式
var __method = this; // this 是匿名函式
return function() {
var a = update([__method.bind(this)], arguments);
return wrapper.apply(this, a);
}
}
wrapper這時候是subclass的function,而__method就是上面create出的匿名函式,然後因為bind(this),所以最後呼叫時候以目前物件為scope去呼叫上一層的function。
用法上的話,以prototyp.js官方文件的範例來看,用法上算是很直覺,prototype.js自己本身也是利用自己的繼承架構來實作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // properties are directly passed to `create` method
var Person = Class.create({
initialize: function(name) {
this.name = name;
},
say: function(message) {
return this.name + ': ' + message;
}
});
// when subclassing, specify the class you want to inherit from
var Pirate = Class.create(Person, {
// redefine the speak method
say: function($super, message) {
return $super(message) + ', yarr!';
}
});
var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John: ahoy matey, yarr!" |
還有一點關於prototype.js的source code是沒有提供minified的版本,並且從裡面很多source code的寫法來看,minify prototype.js應該是會有一些問題,比如說while或是if允許單行本體而不需要以{}包住,但是minified之後很可能就整個爛了。另外現在一般都會建議使用CDN(Content Delivery Network)提供的public library,其中一個好處是如果有cache的話可以很快的載入,而且通常CDN的網路速度以及穩定性應該都會比你自己的好。目前比較常用的是Google CDN
ExtJS
接著可以看一下我覺得實作得非常完整的一套JavaScript library, ExtJS。Sencha已經release ExtJS4的版本了,不過這邊還是看一下我使用比較多的ExtJS3的版本:
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
31
32
33
34
35
36
37
38
| extend : function(){
// inline overrides
var io = function(o){
for(var m in o){
this[m] = o[m];
}
};
var oc = Object.prototype.constructor;
return function(sb, sp, overrides){
if(typeof sp == 'object'){
overrides = sp;
sp = sb;
sb = overrides.constructor != oc ? overrides.constructor : function(){sp.apply(this, arguments);};
}
var F = function(){},
sbp,
spp = sp.prototype;
F.prototype = spp;
sbp = sb.prototype = new F();
sbp.constructor=sb;
sb.superclass=spp;
if(spp.constructor == oc){
spp.constructor=sp;
}
sb.override = function(o){
Ext.override(sb, o);
};
sbp.superclass = sbp.supr = (function(){
return spp;
});
sbp.override = io;
Ext.override(sb, overrides);
sb.extend = function(o){return Ext.extend(sb, o);};
return sb;
};
}() |
ExtJS的繼承實作囉及其實很簡單,基本上就是把基本原則做一次,如果需要往繼承體系的上層呼叫也就是很簡單的直接call superclass的function然後以apply或call改變this scope:
1
2
3
4
5
6
7
8
9
10
11
12
| Ext.Base = Ext.extend(Object, {
member: 0,
constructor: function(){
}
}
Ext.Sub = Ext.extend(Ext.Base, {
member: 0,
constructor: function(){
Ext.Sub.superclass.apply(this, arguments);
}
} |
簡單易用,而且寫法上也比較有模擬出類似一般class繼承的寫法。不過缺點是商業使用是要錢的喔。
jQuery
很著名的jQuery本身並沒有提供class繼承的寫法,但是其作者John Resig倒是有一篇文章: Simple JavaScript Inheritance提供了一個做法去實作出對每個instance function可以去呼叫對應的superclass版本。
結論
總結來看,我個人比較欣賞的是Douglas Crockford提供的方法,很簡短的code達到目的,不過就是使用上比較不直覺。而Prototype.js是我去年用的還滿多的一套JS library,好處是對於DOM element的包裝很完整,壞處就是overhead太高,在一般普遍的benchmark中往往是敬陪末座,在class的用法上基本上也還算是易用,但是背後的實作太過複雜,在不管是空間與時間的複雜度來說都顯得比較高。總結來說,我個人會選擇ExtJS,如果不考慮錢的話,或者是把Douglas Crockford的方法擴充到一個你慣用的library裡(反正JavaScript的擴充你想動就可以動)。
–
本來只是想要做個筆記,想不到寫這篇寫了好幾個禮拜。
圖片來源:
- http://www.w3avenue.com/2010/04/04/js-class-ruby-style-object-oriented-javascript/
- http://www.zazzle.com/