物件導向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);
接下來看一下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其實有很大的不同,所以必須很清楚這樣做是為什麼,做出這些效果跟程式運作的來龍去脈有甚麼關係等等,否則有可能不小心就破壞了這些效果,或是達不到目的。
簡單的結論:
- javascript有prototype base繼承與動態copy(assign)繼承兩種繼承方法
- 必須熟悉javascript的規則,然後再應用這些規則來做出物件導向的效果。