轉自:http://fdream.net/blog/article/604.aspx
這篇文章是Yahoo!的一名資深開發人員寫的,對于JavaScript的弱類對象及其繼承方式講得非常透徹,文章寫得很好,而自己最近又很有點翻譯欲望,于是也一并翻譯過來了。另外,MooTools 1.2.1已經釋出了,修複了一些bug。
原文位址:JavaScript’s class-less objects
請尊重個人勞動,轉載請注明出處: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屬性
- 最後,有很多方式來實作繼承,你可以任意挑選,這完全取決于你的手頭任務、你個人喜好、團隊喜好、你的心情或者目前的月相。
作者及聲明
Stoyan Stefanov是一名資深Yahoo!開發者,YSlow工具的上司、開源貢獻者,部落格作者和技術作者,最近由Packt出版的《Object-Oriented JavaScript》(《面向對象的JavaScript》)的作者。
shiny object的例子靈感來源于Jim Bumgardner的《Theory of the Precious Object 》。
參考《Douglas Crockford’s beget object》