物件導向Javascript - 實作繼承的效果

 Mon, 14 Apr 2008 20:00:53 +0800

關於物件導向javascript的繼承,應該是被討論最多的主題了。有興趣的話,Core JavaScript Guide文件裡面有非常詳細的說明,基本上按照它的說明就可以實作出來。(請參考裡面的Details of the Object Model章節)

底下用一個最簡單的例子做出繼承的效果:

function ancestor(_name) {
this.name = _name||"";
}
function child(_name, _generation) {
this.base = ancestor;
this.base(_name);
this.generation = _generation||0;
}
child.prototype = new ancestor;

這個方式,就是javascript經典的prototype base繼承。透過指定prototype屬性,便可以指定要繼承的目標。在child中,利用:

this.base = ancestor;
this.base(_name);
這兩行便可以呼叫父類別的constructor。原理是,this.base會assign成function ancestor,然後執行。執行時,傳給這個function的this會是child的instance的execution context,所以就把ancestor裡面做的事情在child裡面也重複了一遍。除了用這個方法以外,也可以透過呼叫this.call來達成。(不能使用this.constructor,因為它永遠指向繼承關係最上層的constructor,這樣在繼承超過兩層就會出問題)

接下來看一下prototype繼承的可能方式。其實可以繼承一個....匿名函數,例如:

function test(_task,_name) {
this.constructor(_name);
this.task = _task;
}
test.prototype = new (function(_name){
this.name = _name||"";
});
var a = new test("coding","fillano");
alert(a.name);
alert(a.task);

執行上面的例子,會依序跳出"fillano"、"coding"訊息對話框。(抱歉,這裡用了this.constructor,超過兩層繼承會出問題。但是不這樣的話,沒辦法傳參給anonymous function的constructor...。所以繼承一個anonymous function並不是個好主意,只是做的到而已。)

也可以繼承一個Native Object例如....window物件:

function Workerbee() {
this.task = [];
this.tid = null;
this.test = function() {
this.tid = this.setTimeout(this.test1,100);
}
this.test1 = function() {
this.clearTimeout(this.tid);
this.alert("you got me");
}
}
Workerbee.prototype = window;
var a = new Workerbee();
a.alert("test");
a.test();

執行上面的例子,會依序跳出"test"、"you gotme"訊息對話框。(其他javascript的native object則要用new喔。其實很多native object無法這樣繼承的,它會做內部檢查,不讓你用它的constructor傳參數給他,也會檢查執行方法的物件型別,型別不是自己就出現錯誤。所以一般是不會繼承native object的,而是用object.prototype.x=y的方式加料上去。)

Core JavaScript Guide還提到不少要實作出不同效果的一些方法細節,像是用new產生instance時,在constructor裡面遞增一個計數;如何透過object.prototype.propertyName=value的方式,一次修改所有繼承體系中的propertyName的值等等(方法:propertyName屬性必須在constructor外面利用object.prototype.propertyName的方式定義)。

其實還有更動態的繼承方法,看看下面的例子:

function Parent(_name) {
this.name = _name||"";
this.show = function() {
alert(this.name);
}
}
function Child(_gender) {
this.gender = _gender;
this.mate = function() {
alert(this.gender);
}
this.inherits = function(obj) {
for(var i in (new obj)) {
this[i] = (new obj)[i];
}
}
}
//for(var i in (new Parent)) {
//	Child.prototype[i] = (new Parent)[i];
//}
var a = new Child("male");
a.inherits(Parent);
a.name = "fillano";
a.show();

如果不需要使用constructor,可以用這方法把另一個物件中的所有屬性與方法拷貝到目標物件中。在javascript裡面,assign這個動作是無遠弗屆的!!!(註解的程式與inhertis方法是同義的。另外,這個方法對於window無效。其實要拷貝constructor也是可以的,功能比較完整的例子,可以參考Douglas Crockford的文章:Classical Inheritance in Javascript,裡面有詳細地討論javascript的繼承。除了Core JavaScript Guide之外,這篇應該是物件導向Javascript的必讀文章了。Crockford還有許多討論Javascript特性的文章,都很值得一讀。個人很多觀念都是這裡學來的。我這裡用的方法只是簡單地看出效果,要實用的話,最好參考Crockford的方法。另外,我以前在google feed api產生的javascript也看過類似的作法:))

在Core JavaScript Guide中有提到,prototype base的繼承方法不支援多重繼承,因為改了prototype以後,整個prototype chain就改了,永遠就只有一條single chain的prototype chain,所以永遠也不能多重繼承。但是因為javascript動態的特性,其實用上面這個方法,是可達到多重繼承的目的,只是無法利用到constructor。

其實仔細研究這些繼承的過程,可以發現,javascript只是依照我們要它做的事情一一做好,而透過這些動作,就可以做出我們要的物件導向效果。所以要達到我們想要的目的,就必須一一按照必要的方式把它做出來。prototype繼承、封裝等都是適當地安排好程式做出來的效果。適當使用var、this、function等就可以做出資料封裝的效果;同樣適當地使用prototype、constructor等就能做出繼承的效果。但是這些與原生的物件導向語言例如.....java其實有很大的不同,所以必須很清楚這樣做是為什麼,做出這些效果跟程式運作的來龍去脈有甚麼關係等等,否則有可能不小心就破壞了這些效果,或是達不到目的。

簡單的結論:

  1. javascript有prototype base繼承與動態copy(assign)繼承兩種繼承方法
  2. 必須熟悉javascript的規則,然後再應用這些規則來做出物件導向的效果。