迷霧中的javascript的建構子

 Fri, 07 Sep 2007 20:56:29 +0800

最近在嘗試用observer來自定網頁程式的內部事件/訊息機制,為了方便,打算讓每個函數物件在產生的時候有一個屬性是唯一的key。這樣在observable中加入/移除observer時可以用這個key檔做索引,比較方便。結果在用建構子的時候發生了問題....

在Javascript中,要做出繼承的效果,根據"Core Javascript Guide"裡面"Details of the Object Model"這一節的介紹,是用以下的方法:

1
2
3
4
5
6
7
8
function Parent () {
code....;
}
function Child () {
code....;
}
Child.prototype = new Parent;
var a = new Child();

我原先想要做的Observer像這樣:

1
2
3
4
5
6
7
8
function Observer () {
this.stat = null;
this.uid = (new Date()).getTime().toString() 
+ (Math.random()*100000000).toString();
this.update = function (stat) {
this.stat = stat;
}
}

這樣在用var a = new Observer();時並不會出問題,constructor會在產生a時給a.uid一個不容易重複的key。但是如果要繼承,就會出問題:

1
2
3
4
5
6
7
8
function EventListener () {
this.update = function (stat) {
this.stat = stat;
alert("eventlistener"+stat);
}
}
EventListener.prototype = new Observer;
var a = new EventListener();

關鍵在於,uid在EventListener.prototype = new Observer時候就產生了!所以每個EventListener的instance都有一個固定的uid。要怎麼解決這個問題呢?最初的想法,是在constructor傳入uid作為參數,但是要每次new的時候都打這一串產生uid的方法,感覺很麻煩。所以,我想到簡單的解法是再加一個工廠函數來產生對應的instance:

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
function Observer (uid) {
this.stat = null;
this.uid = uid;
this.update = function (stat) {
this.stat = stat;
}
}
function ObserverFactory (objname) {
try {
return eval("new " + objname + "('" 
+ (new Date()).getTime().toString() 
+ (Math.random()*100000000).toString() + "')");
} catch (e) {
alert(e);
}
}
function EventListener (uid) {
this.uid = uid;
this.update = function (stat) {
this.stat = stat;
alert("eventlistener"+stat);
}
}
EventListener.prototype = new Observer;
var a = ObserverFactory("EventListener");

這樣是可以解決問題,但是真的需要這麼複雜嗎?回頭翻了一下reference,每個物件都有一個constructor屬性,指向自己的constructor。檢查一下a.constructor,會發現透過prototype的方式做出繼承效果,instance物件的constructor都會指向最上層的constructor。例如EventListener繼承Observer,而PhaseListener繼承EventListener,那PhaseListener的instance,constructor屬性就會指向Observer()!所以只要在本身的function definition呼叫this.constructor()就會呼叫到上層的constructor。程式修改一下就便得很簡潔了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Observer () {
this.stat = null;
this.uid = (new Date()).getTime().toString() 
+ (Math.random()*100000000).toString();
this.update = function (stat) {
this.stat = stat;
}
}
function EventListener () {
this.constructor();
this.update = function (stat) {
this.stat = stat;
alert("eventlistener"+stat);
}
}
EventListener.prototype = new Observer;
var a = new EventListener();

嗯嗯,沒弄清楚的原因,是自己在還沒熟悉ECMA-262之前做了一些假設,實際上javascript的建構子運作跟我們習慣的c、java等是不太一樣的,要執行父代的建構子,必須明確執行。另外Javascript的assign非常有彈性,隨時assign隨時參考的物件、屬性、函數等就改了。在最後的程式碼的EventListener裡面呼叫this.constructor(),就會執行function Observer()裡面的程式,在上面的例子裡面是使this.stat=null,然後將this.uid設為一個不易重複的字串,然後assign一個匿名函數給this.update。呼叫完this.constructor()以後,會再重新assign另一個在function EventListener中定義的匿名函數給this.update。運作的模式大概就是像這樣。如果不呼叫this.constructor(),在之前EventListener.prototype = new Observer時其實就呼叫一次了,但是這會使所有EventListener的instance的uid固定不變,再呼叫一次this.constructor()才會正確設定好uid。這樣看起來,需要的話,最好在函數(EventListener)一開始就呼叫this.constructor(),如果在定義一些屬性或函數之後再呼叫this.constructor(),就有可能把之前定義的東西覆蓋過去了(如果名稱相同的話)。換句話說,就是把覆載的屬性/函數用父代的覆蓋過去了,這樣覆載就不會產生效果了。

為了簡化問題,另外做了一個實驗:

1
2
3
4
5
6
7
8
9
function base () {
alert("type of base");
this.type = "base";
}
function derived () {
alert("type of derived");
this.type = "derived";
}
derived.prototype = new base;

只執行以上的程式,就會跳出"type of base"訊息對話框。如果再加上一行:

1
var a = new derived();

會依序跳出"type of base"以及"type of derived"兩個對話框,很明顯地,base()只在derived.prototype=new base()時執行過一次,如果只是單純地assign一些屬性或函數,這樣做是沒問題的,但是如果是希望一些屬性在new的時候動態被父代的constructor決定,就行不通了。但是透過prototype的動作,a的constructor屬性已經被assign成base(),所以只要在derived()裡面呼叫this.constructor(),就可以達到動態使用父代的constructor來決定屬性值的效果。