JavaScript的弱類物件及繼承實現方式
這篇文章是Yahoo!的一名資深開發人員寫的,對於JavaScript的弱類物件及其繼承方式講得非常透徹,文章寫得很好,而自己最近又很有點翻譯慾望,於是也一併翻譯過來了。另外,MooTools 1.2.1已經發布了,修復了一些bug。
請尊重個人勞動,轉載請註明出處:http://fdream.net,譯者:Fdream
Java和JavaScript是相差極大的兩種語言,儘管他們的名字非常像,而且都有類C的語法風格,很多時候這讓人們很迷惑。(Fdream注:曾有人在論壇上問Java和JavaScript是什麼關係?我一師兄的回答非常經典:雷鋒和雷峰塔的關係。)我們來看看兩者最主要的區別——物件是怎樣建立的。在Java中,你有類。然後是物件,又叫例項,都是基於那些類建立的。而在JavaScript中,沒有類存在,物件更像是一個包含鍵值對(key-value pair)的雜湊表(hash table)。然後繼承是什麼樣的呢?好,我們一步一步來。
JavaScript物件
當你考慮一個JavaScript物件時,想一下hash。它們和物件完全一樣——它們都是名值對(name-value pair)集合,值可以是其它任何東西,包括物件和函式。當一個物件的屬性是函式的時候,你也可以叫它們方法。
這是一個空物件:
var myobj = {};
現在,你可以開始給這個物件新增一些有意義的功能:
myobj.name = "My precious";
myobj.getName = function() {return this.name};
注意這樣一些事情:
- 在方法中,this指向當前物件,和期望一樣
- 你可以在任何時候新增、修改、刪除屬性,不限於建立的時候
另一種建立物件並同時新增屬性或者方法的方式是這樣的:
var another = {
name: 'My other precious',
getName: function() {
return this.name;
}
};
這種語法就叫做“物件列舉表示法”(object literal notation)——你把所有的東西都包含在花括號 { 和 } 之間,並用逗號在物件內部區分每個屬性。鍵值對(key:value pair)則以冒號分割。這中語法也不是建立物件的唯一方式。
建構函式
另一種建立JavaScript物件的方式就是使用建構函式(constructor function)。這裡是一個建構函式示例:
function ShinyObject(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
現在,我們可以更像Java那樣建立一個物件:
var my = new ShinyObject('ring');
var myname = my.getName(); // "ring"
建立一個建構函式的語法和其他函式沒有任何區別,唯一的區別是它們的用法不一樣。如果你用new關鍵字來呼叫一個方法,它會建立並且返回一個物件。通過使用this關鍵字,你可以在它返回之前修改這個物件。作為約定俗成的習慣,建構函式的命名通常以一個大寫字母開頭,以區分於其他一般函式和方法。
哪一種方式更好呢?物件列舉還是建構函式?這完全取決於你指定的任務。例如,如果你需要建立許多不同的,但是類似的物件,使用類類(class-like)的建構函式可能才是正確的選擇。但是,如果你的物件更接近於一個單例(singleton),物件列舉方式肯定要更簡單更簡短。
好了,如果沒有類,那麼哪來繼承呢?在我們回答這個問題之前,這裡還有一點驚喜——在JavaScript中,函式(function)也是實際物件。
(實際上,在JavaScript中,幾乎所有東西都是一個物件,出了一些元資料型別——字串(string)、布林值(boolean)、數字(number)和undefined。函式(function)是物件,陣列(array)是物件,甚至null也是一個物件。而且,元資料型別也可以轉換並作為物件使用,因此”string.length“是有效的。)
函式物件和原型物件
在JavaScript中,函式是物件。他們可以賦值給變數,你可以給它們新增屬性和方法等等。這裡是一個函式的示例:
var myfunc = function(param) {
alert(param);
};
這和下面的幾乎一樣:
function myfunc(param) {
alert(param);
}
不管你通過什麼方式建立這個函式,它最後都成為了一個myfunc物件,你可以得到它們的屬性和方法:
alert(myfunc.length); // 顯示 1, 引數個數
alert(myfunc.toString()); // 顯示這個函式的原始碼
一個有趣的屬性是——每個函式物件都有一個prototype屬性。一旦你建立一個函式,它就會自動獲得一個prototype屬性,這個屬性指向一個空的物件。當然,你可以修改那個空物件的屬性。
alert(typeof myfunc.prototype); // 顯示 "object"
myfunc.prototype.test = 1; // 這是完全可以的
問題是:這個原型物件有什麼用呢?只有當你把一個函式作為建構函式呼叫來建立一個物件時有用。當你這麼做的時候,這個物件自動地獲得一個祕密連結指向原型物件的屬性,並可以把這些屬性當作自己的屬性一樣訪問。迷惑了?讓我們看一個例子:
一個新函式:
function ShinyObject(name) {
this.name = name;
}
給這個函式的原型屬性增加一些功能:
ShinyObject.prototype.getName = function() {
return this.name;
};
把這個函式作為建構函式使用,來建立一個物件:
var iphone = new ShinyObject('my precious');
iphone.getName(); // returns "my precious"
正如你所看到的,新的物件自動獲得了原型物件的屬性。當一些功能可以”免費“地獲得的時候,這就有點像是程式碼重用和繼承了。
通過原型繼承
現在我們來看看,你如果通過使用原型來實現繼承。
這裡是一個建構函式,將會作為父類(parent)使用:
function NormalObject() {
this.name = 'normal';
this.getName = function() {
return this.name;
};
}
這裡是第二個建構函式:
function PreciousObject(){
this.shiny = true;
this.round = true;
}
現在是繼承部分:
PreciousObject.prototype = new NormalObject();
可不是嘛!現在你可以建立一個珍寶(precious)物件,然後它們會得到所有普通物品(normal)物件的功能:
var crystal_ball = new PreciousObject();
crystal_ball.name = 'Ball, Crystal Ball.';
alert(crystal_ball.round); // true
alert(crystal_ball.getName()); // "Ball, Crystal Ball."
注意到我們為什麼需要使用new來建立一個物件,然後把它賦值給原型,因為原型僅僅只是一個物件。不像一個建構函式繼承於其它的,本質上,我們從一個物件繼承。JavaScript沒有類從其他類繼承,只有物件從其他物件繼承。
如果你有幾個建構函式都要從NormalObject繼承,你也許需要每次都建立一個new NormalObject(),但是這是沒有必要的。甚至連整個NormalObject建構函式都不是必須的。另外一種實現方式就是建立一個單例普通物件,然後把它作為其他物件的基類使用。
var normal = {
name: 'normal',
getName: function() {
return this.name;
}
};
然後PreciousObject就可以像這樣繼承了:
PreciousObject.prototype = normal;
通過複製屬性繼承
因為繼承只是為了程式碼重用,因此還有一種實現方式就是簡單地複製屬性。
假設你有這些物件:
var shiny = {
shiny: true,
round: true
};
var normal = {
name: 'name me',
getName: function() {
return this.name;
}
};
怎樣讓shiny得到normal的屬性呢?這裡有一個簡單的extend()函式,可以迴圈遍歷並賦值屬性:
function extend(parent, child) {
for (var i in parent) {
child[i] = parent[i];
}
}
extend(normal, shiny); // inherit
shiny.getName(); // "name me"
現在這個屬性賦值看起來像是額外的開銷,而且效能也不是很好,但是事實是,在大多數情況下它還是很好的。你也可以看見——這是一種實現混合繼承和多重繼承的簡單方式。
Crockford的beget object
Douglas Crockford,一代JavaScript大師,JSON的創造者,提出了這樣一種有趣的begetObject()方式來實現繼承:
function begetObject(o) {
function F() {}
F.prototype = o;
return new F();
}
這裡你建立了一個臨時建構函式,因此你可以使用原型功能,這個目的在於你建立了一個新的物件,不過不是一個全新的物件,而是從其它物件那裡繼承了一些已經存在的功能。
父物件:
var normal = {
name: 'name me',
getName: function() {
return this.name;
}
};
一個從父物件繼承的新物件:
var shiny = begetObject(normal);
給這個新物件增加更多功能:
shiny.round = true;
shiny.preciousness = true;
YUI的extend()
讓我們來總結一下另一種方式來實現繼承,這可能是最接近Java的,因為在這種方法中,它看起來像一個建構函式繼承自其它建構函式,因此她看起來有一點像從一個類繼承。
在非常受歡迎的YUI JavaScript庫(Yahoo! User Interface)中已經使用了這種方法,這裡是一個簡單的版本:
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
}
通過這個方法,你傳遞兩個建構函式,第一個(子類)將會通過原型(prototype)屬性得到第二個(父類)的所有屬性和方法。
總結
讓我們很快地總結一下我們剛才所學的有關JavaScript的內容:
- JavaScript中沒有類
- 物件從物件繼承
- 物件列舉表示法 var o = { };
- 建構函式提供類Java語法 var o = new Object();
- 函式是物件
- 所有的函式物件都有一個prototype屬性
- 最後,有很多方式來實現繼承,你可以任意挑選,這完全取決於你的手頭任務、你個人喜好、團隊喜好、你的心情或者當前的月相。
作者及宣告