標準化麵象對象代碼 編寫可重用代碼的第一個也是最重要的步驟就是以一種貫穿整個應用程序的標準方式編寫你的代碼,尤其是麵向對象的代碼。通過上一章的麵向對象的JavaScript的運作方式,你可以看到JavaScript語言相當靈活,允許你模擬許多種不同的編程風格。
作為開端,設計出一種最符合你需要的編寫麵向對象代碼並實現對象繼承(把對象的屬性克隆到新的對象裏)的體製是很重要的。然而表麵看來,每一個寫過一些麵向對象的JavaScript代碼的人都已經建立起了各自的實現方案,這可能相當令人困惑的。在這一節中,我們將弄清JavaScript的中繼承是怎樣工作的,隨後了解幾種不同的供選擇的輔助方法的原理以及怎樣將它們應用於你的程序當中。
原型繼承 JavaScript使用了一種獨特的對象創建和繼承的方式,稱為原型繼承(prototypal inheritance)。這一方法背後的前提(相對大多數程序員所熟悉的傳統的類/對象方案而言)是,一個對象的構造器能夠從另一個對象中繼承方法,建立起一個原型對象,所有的新的對象都將從這個原型創建。
這整個過程由prototype屬性(存在於每一個函數中,因為任何函數都可以是一個構造器)促成。原型繼承是為單繼承設計的;盡管如此,仍然存在可以實現多繼承的手段,我將在下一節中討論。
使得這種形式的繼承特別難以掌握的是,原型並不從其它的原型或者其它的構造器繼承屬性,而是從實際的對象中繼承。程序3-1展示了prototype屬性怎樣被用於簡單繼承的幾個例子。
程序3-1. 原型繼承的例子
//創建Person對象的構造器
function Person( name ) {
this.name = name;
}
//為Person對象加入一個新方法
Person.prototype.getName = function() {
return this.name;
};
//創建一個新的User對象構造器
function User( name, password ) {
//注意這並不支持優雅的重載/繼承,
//如能夠調用超類的構造器
this.name = name;
this.password = password;
};
//User對象繼承Person對象的全部方法
User.prototype = new Person();
//我們添加一個自己的方法給User對象
User.prototype.getPassword = function() {
return this.password;
};
上例中最重要的一行是User.prototype = new Person();。我們來深入地看看這到底意味著什麼。User是對User對象的函數構造器的引用。new Person()建創一個新的Person對象,使用Person構造器。將這一結果設為User構造器的prototype的值,這意味著不論任何時候你使用new User()的時候,新建的User類對象也將擁有你使用new Person()時創建的Person類對象的所有方法。
帶著這一特殊的技巧,我們來看一些不同的開發者所編寫的使得JavaScript中繼承的過程簡單化的封裝。
類繼承 類繼承(classical inheritance)是多數開發者所熟悉的一種形式,擁有帶方法的可被實例化為對象的類。對初學麵向對象JavaScript的程序員來說這種情況是非常典型的:試圖模擬這種程序構思,卻很少真正悟出怎樣正確地實現。
值得感激的是,JavaScript大師之一,Douglas Crockford,把開發一套能用於JavaScript模擬類式繼承的簡單方法做為了他的目標,如他在網站上所解釋的那樣(
https://javascript.crockford.com/inheritance.html)。
程序3-2展示了他所編寫的三個函數,用來建立起一種類風格的JavaScript繼承的綜合形式。每個函數實現了繼承的一個方麵:繼承單個函數,繼承單個父類的全部,和從多個父類中繼承獨立的方法。
程序3-2. Douglas Crockford的使用JavaScript模擬類形式繼承的三個函數
//一個簡單的輔助函數,允許你為對象的原型綁定新的函數
Function.prototype.method = function(name, func) {
this.prototype[name] = func;
return this;
};
//一個(相當複雜的)函數,允許你優雅地從其它對象中繼承函數,
//同時仍能調用"父"對象的函數
Function.method('inherits', function(parent) {
//追蹤所處的父級深度
//Keep track of how many parent-levels deep we are
var depth = 0;
//繼承parent的方法
//Inhert the parent's methods
var proto = this.prototype = new parent();
//創建一個名為uber的新的特權方法,
//調用它可以執行在繼承中被覆蓋的任何函數
//Create a new 'priveledged' function called 'uber', that when called
//executes any function that has been written over in the inheritance
this.method('uber', function uber(name) {
var func; //將被執行的函數(The function to be execute)
var ret; // 該函數的返回值(the return value of then function)
var v = parent.prototype; //父類的prototype(The parent's prototype)
//如果已經位於另一"uber"函數內
//If we're already within another 'uber' function
if (depth) {
//越過必要的深度以找到最初的prototype
//Go the necessary depth to function the orignal prototype
for ( var i = d; i > 0; i += 1 ) {
v = v.constructor.prototype;
}
//並從該prototype取得函數
//and get the functin from that prototype
func = v[name];
//否則,這是第一級的uber調用
//Otherwise, this is the first 'uber' call
} else {
//從prototype中取得函數
//Get the function to execute from the prototype
func = proto[name];
//如果該函數屬於當前的prototype
//If the function was a part of this prototype
if ( func == this[name] ) {
//則轉入parent的prototype替代之
//Go to the parent's prototype instead
func = v[name];
}
}
//記錄我們位於繼承棧中的'深度'
//Keep track of how 'deep' we are in the inheritance stack
depth += 1;
//使用用第一個參數後麵的所有參數調用該函數
//(第一個參數保有我們正在執行的函數的名稱)
//Call the function to execute with all the arguments but the first
//(whick holds the name of the function that we're executing)
ret = func.apply(this, Array.prototype.slice.apply(arguments, [1]));
//重置棧深度
//Reset the stack depth
depth -= 1;
//返回執行函數的返回值
//Return the return value of the execute function
return ret;
});
return this;
});
//一個用來僅繼承父對象中的幾個函數的函數,
//而不是使用new parent()繼承每一個函數
Function.method('swiss', function(parent) {
//遍曆所有要繼承的方法
for (var i = 1; i < arguments.length; i += 1) {
//要導入的方法名
var name = arguments[i];
//將方法導入這個對象的prototype
this.prototype[name] = parent.prototype[name];
}
return this;
});
我們來看看這三個函數到底提供給我們些什麼,以及為什麼我們應該使用它們而不去試圖寫出我們自己的原型繼承模型。這三個函數的前提是簡單的:
Function.prototype.method:此函數是為構造器的prototype附加函數的簡單方式。這一特殊的子句能夠工作是因為所有的構造器都是函數,故能獲得新的方法"method"。
Function.prototype.inherits:這一函數能用來提供簡單的單父繼承。函數代碼的主體圍繞著在你的對象的任何方法中調用this.uber("方法名")使之執行它所重寫了的父對象的方法的能力。這是JavaScript繼承模型本身不具備的一個方麵。
Function.prototype.swiss:這是.method()函數的一個高級版本,能用來從一個父對象中抓取多個方法。當將它分別用於多個父對象時,你將得到一種實用的多父繼承的形式。
對上麵三個函數提供給我們什麼有了一個大致的了解之後,程序3-3重拾你在3-1中所見的Person/User的例子,不過這次使用了新的類風格的繼承。另外,你可以看看在改善程序清晰性方麵,這個庫能夠提供怎樣的額外功能。
程序3-3. Douglas Crockford的類繼承式JavaScript函數的例子。
//創建一個新的Person對象構造器
function Person( name ) {
this.name = name;
}
//給Person對象添加方法
Person.method( 'getName', function(){
return name;
});
//創建新一個新的User對象構造器
function User( name, password ) {
this.name = name;
this.password = password;
},
//從Person對象繼承所有方法
User.inherits( Person );
//給User對象添加一個新方法
User.method( 'getPassword', function(){
return this.password;
});
//重寫新Person對象創建的方法,
//但又使用uber函數再次調用它
User.method( 'getName', function(){
return "My name is: " + this.uber('getName');
});
嚐試過使用一個可靠的繼承加強的JavaScript庫所帶來的可能性之後,我們再來關注其它的一些廣通用的流行的方法。
Base庫 JavaScript對象創建和繼承領域近期的成果是Dean Edwards所開發的Base庫。這一特別的庫提供了一些不同的方式來擴展對象的功能。除此之外,它甚至提供了一種直覺式的對象繼承方式。Dean最初開發這個庫是為了用於他的其它的項目,包括IE7項目(作為對IE一整套的升級)。Dean的網站上列出的例子相當易於理解並確實很好的展示了這個庫的能力:
https://dean.edwards.name/weblog/2006/03/base 。除此而外,你可以在Base源代碼目錄裏找到更多的例子:
https://dean.edwards.name/base/。
Base庫是相當冗長而複雜的,它值得用額外的注釋來說明(包含於
https://www.apress.com的Source Code/Download所提供的代碼中)。除了通讀注釋過的代碼以外,強烈建議你去看Dean在他的網站上提供的例子,因為它們非常有助於澄清常見的疑惑。
但作為起點,我將帶你一覽Base庫的幾個可能對你的開發很有幫助的重要的方麵。具體地,在程序3-4展示了類創建、單父繼承和重寫父類函數的例子。
程序3-4. 利用Dean Edwards的Base庫進行簡單的類創建和繼承的例子
//創建一個新的Person類
var Person = Base.extend({
//Person類的構造函數
constructor: function( name ) {
this.name = name;
},
//Person類的簡單方法
getName: function() {
return this.name;
}
});
//創建一個新的繼承了Person類的User類
var User = Person.extend({
//創建User類的構造器,
constructor: function( name, password ) {
//該構造器順次調用了父類的構造器方法
this.base( name );
this.password = password;
},
//為User類創建另一個簡單的方法
getPassword: function() {
return this.password;
}
});
我們來看看在程序3-4中Base庫是如何達到先前所歸納的三個目標從而創造出一種對象創建和繼承的簡單形式的。
Base.extend(...);:這一表達式用來創建一個新的基本的構造器對象。此函數授受一個參數,即一個簡單的包含屬性和值的對象,其中的屬性都會作為原型方法被被增添到(所創建的構造器)對象中。
Person.extend(...);:這是Base.extend()語法的一個可替換版本。所有的創建的構造器都使用.extend()方法獲取它們自己的.extend()方法,這意味著直接從它們繼承是可能的。程序3-4中,正是通過直接從最初的Person構造器中直接繼承的方式創建了User構造器。
this.base();:最後,this.base()方法用來調用父對象的被重寫了的對象。你會發現這與Corockford's的類繼承所使用的this.uber()函數截然是截然不同的,你無需提供父類的方法名(這一點有助於真正地清理並明晰化你的代碼)。在所有的麵向對象的JavaScript庫中,Base庫的重寫父方法的功能是最好的。
個人而言,我覺得Dean的Base庫能夠出產最可讀的、實用的和可理解的麵向對象的JavaScript代碼。當然,最終選擇什麼庫要看開發者自己覺得什麼最適合他。接下來你將看到麵對對象的JavaScript代碼如何在流行的Prototype庫中實現。
Prototype庫 Prototype是一個為了與流行的"Ruby on Rails"web框架協同工作而發的JavaScript庫。不要把庫的名字與構造器的prototype屬性混淆——那是隻一種令人遺憾的命名情況。
撇開命名不談,Prototype庫使得JavaScript外觀和行為上者更接近於Ruby。為達到這一點,Prototype的開發者們利用了JavaScript的麵向對象本質,並且附加了一些函數和屬性給核心的JavaScript對象。不幸的是,該庫根本不是由它的創造者們給出文檔的;而幸運的是它寫得非常清晰,而且它的一些用戶介入編寫了他們自己版本的文檔。你們可以在Prototype的網站(
https://prototype.conio.net/)上隨意地瀏覽完整的代碼,從文章
"Painless JavaScript Using Prototype"裏得到Prototype的文檔。
在這一節裏,我們將僅著眼於Prototype用於創建其麵象對象結構並提供基本繼承的特定的函數和對象。程序3-5展示了Prototype使用的達到此目標的全部代碼。
程序3-5. Prototype所使用的模擬麵向對象JavaScript代碼的兩個函數
//創建一個名為"Class"的全局對象
var Class = {
//它擁有一個用來創建新的對象構造器的函數
create: function() {
//創建一個匿名的對象構造器
return function() {
//調用它本身的初始化方法
this.initialize.apply(this, arguments);
}
}
}
//為對象"Object"添加靜態方法,用以從一個對象向另一個對象複製屬性
Object.extend = function(destination, source) {
//遍曆欲擴展的所有屬性
for (property in source) {
//並將它添加到目標對象
destination[property] = source[property];
}
//返回修改過的對象
return destination;
}
Prototype確實隻用了兩個明顯的函數來創建和維護其整個麵向對象體係。你們可能已發現,僅通過看觀察代碼,也能斷定它不如Base或者Crockford的類式方法那樣強大。兩個函數的前提很簡單:
Class.create():這個函數簡單地返回一個可用做構造器的匿名函數包裝。這個簡單的構造器做了一件事:調用和執行對象的initialze屬性。這意味著,你的對象裏至少有一個包含函數的initialize屬性;否則,代碼將會出錯。
Object.extend():這個函數簡單地從一個對象往另一個對象複製屬性。當你使用構造器的prototype屬性時你能設計出一種更簡單的繼承的形式(比JavaScript中可用的缺省的原型繼承更簡單)。
既然你已經了解了Prototype的底層代碼是如何工作的,程序3-6展示了一些例子,說明它在Prototype庫自身中是怎樣用來通過添加功能層來擴展天然的JavaScript對象的。
程序3-6. Prototype怎樣使用麵對對象函數擴展JavaScript中字符串的缺省操作的例子。
//為String對象的原型添加額外的方法
Object.extend(String.prototype, {
//一個新的stripTags函數,刪除字符串中的所有HTML標簽
stripTags: function() {
return this.replace(/<//?[^>]+>/gi, '');
},
//將一個字符串轉換成一個字符的數組
toArray: function() {
return this.split('');
},
//將文本"foo-bar"轉換成'駱駝'文本"fooBar"(譯注:fooBar中間的大寫字符像是駝峰吧)
//Converts "foo-bar" text to "fooBar" 'camel' text
camelize: function() {
//以'-'拆分字符串
var oStringList = this.split('-');
//若字符串中沒有'-'則提前返回
if (oStringList.length == 1)
return oStringList[0];
//隨意地"駱駝化"字符串的開頭
//Optionally camelize the start of the string
var camelizedString = this.indexOf('-') == 0
? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
/*
譯注:this.indexOf('-')==0,那oStringList[0]顯然就是空字符串了,
有必要toUpperCase加substring嗎?
*/
: oStringList[0];
//將後繼部分的首字母大寫
for (var i = 1, len = oStringList.length; i < len; i++) {
var s = oStringList[i];
camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
}
//返回修改的字符串
return camelizedString;
}
});
//stripTags()方法的一個例子
//可以看到它刪除了字符串中的所有HTML
//隻保留純文本
"<b><i>Hello</i>, world!".stripTags() == "Hello, world!"
//toArray()方法的一個例子
//我們將得到字符串中的第四個字符
"abcdefg".toArray()[3] == "d"
//camelize()方法的例子
//它將原字符串轉換成新的格式
"background-color".camelize() == "backgroundColor"
接下來,讓我們再一次回到這章我所用到的那個有著User和Person對象且User對象從Person對象繼承屬性的例子。使用Prototype的麵向對象風格的代碼,見程序3-7。
程序3-7. Prototype的用於創建類和實現簡單繼承的輔助函數
//用名義上的構造器創建一個Person對象
var Person = Class.create();
//將下列的函數複製給Person的prototype
Object.extend( Person.prototype, {
//此函數立即被Person的構造器調用
initialize: function( name ) {
this.name = name;
},
//Person對象的簡單函數
getName: function() {
return this.name;
}
});
//用名義上的構造器創建一個User對象
var User = Class.create();
//User對象從其父類繼承所有屬性
User.prototype = Object.extend( new Person(), {
//用新的初始化函數重寫原來的
initialize: function( name, password ) {
this.name = name;
this.password = password;
},
//為對象添加一個新的函數
getPassword: function() {
return this.password;
}
});
盡管Prototype庫所提出的麵向對象技術不是革命性的,它們也強大到足以幫助開發者創建更簡單、更易編寫的代碼了。然而,如果你正將編寫數量巨大的麵向對象代碼,最終你可能更趨向於選擇Base這樣的庫來輔助你的工作。
接下來我們將探討怎樣處理你的麵向對象的代碼,並使之準備好被其它的開發者或庫所使用並與之相合。
[
本帖最後由 mozart0 於 2007-4-8 12:50 編輯 ]