JavaScript
使用面向對象的技術建立進階 Web 應用程式
本文讨論:
| 本文使用了以下技術: JavaScript |
目錄
JavaScript 對象是詞典
JavaScript 函數是最棒的
構造函數而不是類
原型
靜态屬性和方法
閉包
模拟私有屬性
從類繼承
模拟命名空間
應當這樣編寫 JavaScript 代碼嗎?
展望
最近,我面試了一個有五年 Web 應用程式開發經驗的軟體開發人員。四年半來她一直在從事 JavaScript 相關的工作,她自認為 JavaScript 技能非常好,但在不久之後我就發現實際上她對 JavaScript 知之甚少。話雖這樣說,但我确實沒有責備她的意思。JavaScript 真的是很有趣。很多人(包括我自己,直到最近!)都認為自己很擅長 JavaScript 語言,因為他們都知道 C/C++/C# ,或者有一些以前的程式設計經驗。
在某種程度上,這種假設并不是完全沒有根據的。用 JavaScript 很容易做些簡單的事情。入門的門檻很低,該語言很寬松,它不需要您知道很多細節就可以開始用它進行編碼。甚至非程式設計人員也可能用它在幾個小時内為首頁編寫一些有用的腳本。
的确,直到最近,僅僅憑借 MSDN® DHTML 參考資料和我的 C++/C# 經驗,我也總能勉強利用這點 JavaScript 知識完成一些任務。隻是當我開始編寫真實的 AJAX 應用程式時,我才意識到實際上我的 JavaScript 知識還非常不夠。這個新一代的 Web 應用程式的複雜性和互動性需要程式員以完全不同的方法來編寫 JavaScript 代碼。它們是真正的 JavaScript 應用程式!我們在編寫一次性腳本時一直采用的方法已完全不再有效。
面向對象程式設計 (OOP) 是一種流行的程式設計方法,很多 JavaScript 庫中都使用這種方法,以便更好地管理和維護基本代碼。JavaScript 支援 OOP ,但與諸如 C++ 、C# 或 Visual Basic® 等流行的 Microsoft® .NET Framework 相容語言相比,它支援 OOP 的方式非常不同,是以主要使用這些語言的開發人員開始可能會覺得在 JavaScript 中使用 OOP 很奇怪而且不直覺。我寫本文就是為了深入讨論 JavaScript 語言實際上如何支援面向對象程式設計,以及您如何使用這一支援在 JavaScript 中高效地進行面向對象開發。下面首先讨論對象(還能先讨論其他别的什麼呢?)。
JavaScript 對象是詞典
在 C++ 或 C# 中,在談論對象時,是指類或結構的執行個體。對象有不同的屬性和方法,具體取決于将它們執行個體化的模闆(即類)。而 JavaScript 對象卻不是這樣。在 JavaScript 中,對象隻是一組名稱/ 值對,就是說,将 JavaScript 對象視為包含字元串關鍵字的詞典。我們可以使用熟悉的“.” (點)運算符或“[]” 運算符,來獲得和設定對象的屬性,這是在處理詞典時通常采用的方法。以 下代碼段
var userObject = new Object();
userObject.lastLoginTime = new Date();
alert(userObject.lastLoginTime);
的功能與下面的代碼段完全相同:
var userObject = {}; // equivalent to new Object()
userObject[“lastLoginTime”] = new Date();
alert(userObject[“lastLoginTime”]);
我們還可以直接在 userObject 的定義中定義 lastLoginTime 屬性,如下所示:
var userObject = { “lastLoginTime”: new Date() };
alert(userObject.lastLoginTime);
注 意,它與 C# 3.0 對象初始值非常相似。而且,熟悉 Python 的人會發現在第二和第三個代碼段中執行個體化 userObject 的方法與在 Python 中指定詞典的方法完全相同。唯一的差異是 JavaScript 對象/ 詞典隻接受字元串關鍵字,而不是像 Python 詞典那樣接受可哈希化的對象。
這 些示例還顯示 JavaScript 對象比 C++ 或 C# 對象具有更大的可延展性。您不必預先聲明屬性 lastLoginTime — 如果 userObject 沒有該名稱的屬性,該屬性将被直接添加到 userObject 。如果記住 JavaScript 對象是詞典,您就不會對此感到吃驚了,畢竟,我們一直在向詞典添加新關鍵字(和其各自的值)。
這樣,我們就有了對象屬性。對象方法呢?同樣,JavaScript 與 C++/C# 不同。若要了解對象方法,首先需要仔細了解一下 JavaScript 函數。
JavaScript 函數是最棒的
在很多程式設計語言中,函數和對象通常被視為兩樣不同的東西。在 JavaScript 中,其差别很模糊 — JavaScript 函數實際上是具有與它關聯的可執行代碼的對象。請如此看待普通函數:
function func(x) {
alert(x);
}
func(“blah”);
這就是通常在 JavaScript 中定義函數的方法。但是,還可以按以下方法定義該函數,您在此建立匿名函數對象,并将它賦給變量 func
var func = function(x) {
alert(x);
};
func(“blah2”);
甚至也可以像下面這樣,使用 Function 構造函數:
var func = new Function(“x”, “alert(x);”);
func(“blah3”);
此 示例表明函數實際上隻是支援函數調用操作的對象。最後一個使用 Function 構造函數來定義函數的方法并不常用,但它展示的可能性非常有趣,因為您可能注意到,該函數的主體正是 Function 構造函數的 String 參數。這意味着,您可以在運作時構造任意函數。
為了進一步示範函數是對象,您可以像對其他任何 JavaScript 對象一樣,在函數中設定或添加屬性:
function sayHi(x) {
alert(“Hi, “ + x + “!”);
}
sayHi.text = “Hello World!”;
sayHi[“text2”] = “Hello World... again.”;
alert(sayHi[“text”]); // displays “Hello World!”
alert(sayHi.text2); // displays “Hello World... again.”
作為對象,函數還可以賦給變量、作為參數傳遞給其他函數、作為其他函數的值傳回,并可以作為對象的屬性或數組的元素進行存儲等等。圖 1 提供了這樣一個示例。
Figure 1 JavaScript 中的函數是最棒的
// assign an anonymous function to a variable
var greet = function(x) {
alert(“Hello, “ + x);
};
greet(“MSDN readers”);
// passing a function as an argument to another
function square(x) {
return x * x;
}
function operateOn(num, func) {
return func(num);
}
// displays 256
alert(operateOn(16, square));
// functions as return values
function makeIncrementer() {
return function(x) { return x + 1; };
}
var inc = makeIncrementer();
// displays 8
alert(inc(7));
// functions stored as array elements
var arr = [];
arr[0] = function(x) { return x * x; };
arr[1] = arr[0](2);
arr[2] = arr[0](arr[1]);
arr[3] = arr[0](arr[2]);
// displays 256
alert(arr[3]);
// functions as object properties
var obj = { “toString” : function() { return “This is an object.”; } };
// calls obj.toString()
alert(obj);
記住這一點後,向對象添加方法将是很容易的事情:隻需選擇名稱,然後将函數賦給該名稱。是以,我通過将匿名函數分别賦給相應的方法名稱,在對象中定義了三個方法:
var myDog = {
“name” : “Spot”,
“bark” : function() { alert(“Woof!”); },
“displayFullName” : function() {
alert(this.name + “ The Alpha Dog”);
},
“chaseMrPostman” : function() {
// implementation beyond the scope of this article
}
};
myDog.displayFullName();
myDog.bark(); // Woof!
C++/C# 開發人員應當很熟悉 displayFullName 函數中使用的“this” 關鍵字 — 它引用一個對象,通過對象調用方法(使用 Visual Basic 的開發人員也應當很熟悉它,它在 Visual Basic 中叫做“Me” )。是以在上面的示例中,displayFullName 中的“this” 的值是 myDog 對象。但是,“this” 的值不是靜态的。通過不同對象調用“this” 時,它的值也會更改以便指向相應的對象,如圖 2 所示。
Figure 2 “this” 随對象更改而更改
function displayQuote() {
// the value of “this” will change; depends on
// which object it is called through
alert(this.memorableQuote);
}
var williamShakespeare = {
“memorableQuote”: “It is a wise father that knows his own child.”,
“sayIt” : displayQuote
};
var markTwain = {
“memorableQuote”: “Golf is a good walk spoiled.”,
“sayIt” : displayQuote
};
var oscarWilde = {
“memorableQuote”: “True friends stab you in the front.”
// we can call the function displayQuote
// as a method of oscarWilde without assigning it
// as oscarWilde’s method.
//”sayIt” : displayQuote
};
williamShakespeare.sayIt(); // true, true
markTwain.sayIt(); // he didn’t know where to play golf
// watch this, each function has a method call()
// that allows the function to be called as a
// method of the object passed to call() as an
// argument.
// this line below is equivalent to assigning
// displayQuote to sayIt, and calling oscarWilde.sayIt().
displayQuote.call(oscarWilde); // ouch!
圖 2 中的最後一行表示的是将函數作為對象的方法進行調用的另一種方式。請記住,JavaScript 中的函數是對象。每個函數對象都有一個名為 call 的方法,它将函數作為第一個參數的方法進行調用。就是說,作為函數第一個參數傳遞給 call 的任何對象都将在函數調用中成為“this” 的值。這一技術對于調用基類構造函數來說非常有用,稍後将對此進行介紹。
有 一點需要記住,絕不要調用包含“this” (卻沒有所屬對象)的函數。否則,将違反全局命名空間,因為在該調用中,“this” 将引用全局對象,而這必然 會給您的應用程式帶來災難。例如,下面的腳本将更改 JavaScript 的全局函數 isNaN 的行為。一定不要這樣做!
alert(“NaN is NaN: “ + isNaN(NaN));
function x() {
this.isNaN = function() {
return “not anymore!”;
};
}
// alert!!! trampling the Global object!!!
x();
alert(“NaN is NaN: “ + isNaN(NaN));
到 這裡,我們已經介紹了如何建立對象,包括它的屬性和方法。但如果注意上面的所有代碼段,您會發現屬性和方法是在對象定義本身中進行寫死的。但如果需要更 好地控制對象的建立,該怎麼做呢?例如,您可能需要根據某些參數來計算對象的屬性值。或者,可能需要将對象的屬性初始化為僅在運作時才能獲得的值。也可能 需要建立對象的多個執行個體(此要求非常常見)。
在 C# 中,我們使用類來執行個體化對象執行個體。但 JavaScript 與此不同,因為它沒有類。您将在下一節中看到,您可以充分利用這一情況:函數在與“new” 運算符一起使用時,函數将充當構造函數。
構造函數而不是類
前面提到過,有關 JavaScript OOP 的最奇怪的事情是,JavaScript 不像 C# 或 C++ 那樣,它沒有類。在 C# 中,在執行類似下面的操作時:
Dog spot = new Dog();
将傳回一個對象,該對象是 Dog 類的執行個體。但在 JavaScript 中,本來就沒有類。與通路類最近似的方法是定義構造函數,如下所示:
function DogConstructor(name) {
this.name = name;
this.respondTo = function(name) {
if(this.name == name) {
alert(“Woof”);
}
};
}
var spot = new DogConstructor(“Spot”);
spot.respondTo(“Rover”); // nope
spot.respondTo(“Spot”); // yeah!
那麼,結果會怎樣呢?暫時忽略 DogConstructor 函數定義,看一看這一行:
var spot = new DogConstructor(“Spot”);
“new” 運算符執行的操作很簡單。首先,它建立一個新的空對象。然後執行緊随其後的函數調用,将新的空對象設定為該函數中“this” 的值。換句話說,可以認為上面這行包含“new” 運算符的代碼與下面兩行代碼的功能相當:
// create an empty object
var spot = {};
// call the function as a method of the empty object
DogConstructor.call(spot, “Spot”);
正如在 DogConstructor 主體中看到的那樣,調用此函數将初始化對象,在調用期間關鍵字“this” 将引用此對象。這樣,就可以為對象建立模闆!隻要需要建立類似的對象,就可以與 構造函數一起調用“new” ,傳回的結果将是一個完全初始化的對象。這與類非常相似,不是嗎?實際上,在 JavaScript 中構造函數的名稱通常就是所模拟的類的名稱,是以在上面的示例中,可以直接命名構造函數 Dog :
// Think of this as class Dog
function Dog(name) {
// instance variable
this.name = name;
// instance method? Hmmm...
this.respondTo = function(name) {
if(this.name == name) {
alert(“Woof”);
}
};
}
var spot = new Dog(“Spot”);
在 上面的 Dog 定義中,我定義了名為 name 的執行個體變量。使用 Dog 作為其構造函數所建立的每個對象都有它自己的執行個體變量名稱副本(前面提到過,它就是對象詞典的條目)。這就是希望的結果。畢竟,每個對象都需要它自己的實 例變量副本來表示其狀态。但如果看看下一行,就會發現每個 Dog 執行個體也都有它自己的 respondTo 方法副本,這是個浪費;您隻需要一個可供各個 Dog 執行個體共享的 respondTo 執行個體!通過在 Dog 以外定義 respondTo ,可以避免此問題,如下所示:
function respondTo() {
// respondTo definition
}
function Dog(name) {
this.name = name;
// attached this function as a method of the object
this.respondTo = respondTo;
}
這 樣,所有 Dog 執行個體(即用構造函數 Dog 建立的所有執行個體)都可以共享 respondTo 方法的一個執行個體。但随着方法數的增加,維護工作将越來越難。最後,基本代碼中将有很多全局函數,而且随着“ 類” 的增加,事情隻會變得更加糟糕(如果它們的 方法具有相似的名稱,則尤甚)。但使用原型對象可以更好地解決這個問題,這是下一節的主題。
原型
在 使用 JavaScript 的面向對象程式設計中,原型對象是個核心概念。在 JavaScript 中對象是作為現有示例(即原型)對象的副本而建立的,該名稱就來自于這一概念。此原型對象的任何屬性和方法都将顯示為從原型的構造函數建立的對象的屬性和 方法。可以說,這些對象從其原型繼承了屬性和方法。當您建立如下所示的新 Dog 對象時:
var buddy = new Dog(“Buddy“);
buddy 所引用的對象将從它的原型繼承屬性和方法,盡管僅從這一行可能無法明确判斷原型來自哪裡。對象 buddy 的原型來自構造函數(在這裡是函數 Dog )的屬性。
在 JavaScript 中,每個函數都有名為“prototype” 的屬性,用于引用原型對象。此原型對象又有名為“constructor” 的屬性,它反過來引用函數本身。這是一種循環引用,圖 3 更好地說明了這種循環關系。
圖 3 每個函數的原型都有一個 Constructor 屬性
現在,通過“new” 運算符用函數(上面示例中為 Dog )建立對象時,所獲得的對象将繼承 Dog.prototype 的屬性。在圖 3 中,可以看到 Dog.prototype 對象有一個回指 Dog 函數的構造函數屬性。這樣,每個 Dog 對象(從 Dog.prototype 繼承而來)都有一個回指 Dog 函數的構造函數屬性。圖 4 中的代碼證明了這一點。圖 5 顯示了構造函數、原型對象以及用它們建立的對象之間的這一關系。
Figure 4 對象具有其原型的屬性
var spot = new Dog(“Spot”);
// Dog.prototype is the prototype of spot
alert(Dog.prototype.isPrototypeOf(spot));
// spot inherits the constructor property
// from Dog.prototype
alert(spot.constructor == Dog.prototype.constructor);
alert(spot.constructor == Dog);
// But constructor property doesn’t belong
// to spot. The line below displays “false”
alert(spot.hasOwnProperty(“constructor”));
// The constructor property belongs to Dog.prototype
// The line below displays “true”
alert(Dog.prototype.hasOwnProperty(“constructor”));
圖 5 執行個體繼承其原型
某些讀者可能已經注意到圖 4 中對 hasOwnProperty 和 isPrototypeOf 方法的調用。這些方法是從哪裡來的呢?它們不是來自 Dog.prototype 。實際上,在 Dog.prototype 和 Dog 執行個體中還可以調用其他方法,比如 toString 、toLocaleString 和 valueOf ,但它們都不來自 Dog.prototype 。您會發現,就像 .NET Framework 中的 System.Object 充當所有類的最終基類一樣,JavaScript 中的 Object.prototype 是所有原型的最終基礎原型。(Object.prototype 的原型是 null 。)
在此示例中,請記住 Dog.prototype 是對象。它是通過調用 Object 構造函數建立的(盡管它不可見):
Dog.prototype = new Object();
是以,正如 Dog 執行個體繼承 Dog.prototype 一樣,Dog.prototype 繼承 Object.prototype 。這使得所有 Dog 執行個體也繼承了 Object.prototype 的方法和屬性。
每 個 JavaScript 對象都繼承一個原型鍊,而所有原型都終止于 Object.prototype 。注意,迄今為止您看到的這種繼承是活動對象之間的繼承。它不同于繼承的常見概念,後者是指在聲明類時類之間的發生的繼 承。是以,JavaScript 繼承動态性更強。它使用簡單算法實作這一點,如下所示:當您嘗試通路對象的屬性/ 方法時,JavaScript 将檢查該屬性/ 方法是否是在該對象中定義的。如果不是,則檢查對象的原型。如果還不是,則檢查該對象的原型的原型,如此繼續,一直檢查到 Object.prototype 。圖 6 說明了此解析過程。
圖 6 在原型鍊中解析 toString() 方法 ( 單擊該圖像獲得較大視圖)
JavaScript 動态地解析屬性通路和方法調用的方式産生了一些特殊效果:
- 繼承原型對象的對象上可以立即呈現對原型所做的更改,即使是在建立這些對象之後。
- 如果在對象中定義了屬性/ 方法 X ,則該對象的原型中将隐藏同名的屬性/ 方法。例如,通過在 Dog.prototype 中定義 toString 方法,可以改寫 Object.prototype 的 toString 方法。
- 更改隻沿一個方向傳遞,即從原型到它的派生對象,但不能沿相反方向傳遞。
圖 7 說明了這些效果。圖 7 還顯示了如何解決前面遇到的不需要的方法執行個體的問題。通過将方法放在原型内部,可以使對象共享方法,而不必使每個對象都有單獨的函數對象執行個體。在此示例 中,rover 和 spot 共享 getBreed 方法,直至在 spot 中以任何方式改寫 toString 方法。此後,spot 有了它自己版本的 getBreed 方法,但 rover 對象和用新 GreatDane 建立的後續對象仍将共享在 GreatDane.prototype 對象中定義的那個 getBreed 方法執行個體。
Figure 7 繼承原型
function GreatDane() { }
var rover = new GreatDane();
var spot = new GreatDane();
GreatDane.prototype.getBreed = function() {
return “Great Dane”;
};
// Works, even though at this point
// rover and spot are already created.
alert(rover.getBreed());
// this hides getBreed() in GreatDane.prototype
spot.getBreed = function() {
return “Little Great Dane”;
};
alert(spot.getBreed());
// but of course, the change to getBreed
// doesn’t propagate back to GreatDane.prototype
// and other objects inheriting from it,
// it only happens in the spot object
alert(rover.getBreed());
靜态屬性和方法
有 時,您需要綁定到類而不是執行個體的屬性或方法,也就是,靜态屬性和方法。在 JavaScript 中很容易做到這一點,因為函數是可以按需要設定其屬性和方法的對象。由于在 JavaScript 中構造函數表示類,是以可以通過在構造函數中設定靜态方法和屬性,直接将它們添加到類中,如下所示:
function DateTime() { }
// set static method now()
DateTime.now = function() {
return new Date();
};
alert(DateTime.now());
在 JavaScript 中調用靜态方法的文法與在 C# 中幾乎完全相同。這不應當讓人感到吃驚,因為構造函數的名稱實際上是類的名稱。這樣,就有了類、公用屬性/ 方法,以及靜态屬性/ 方法。還需要其他什麼嗎? 當然,私有成員。但 JavaScript 本身并不支援私有成員(同樣,也不支援受保護成員)。任何人都可以通路對象的所有屬性和方法。但我們有辦法讓類中包含私有成員,但在此之前,您首先需要理 解閉包。
閉包
我 沒有自覺地學習過 JavaScript 。我必須快點了解它,因為我發現如果沒有它,在實際工作中編寫 AJAX 應用程式的準備就會不充分。開始,我感到我的程式設計水準好像降了幾個級别。(JavaScript !我的 C++ 朋友會怎麼說?)但一旦我克服最初的障礙,我就發現 JavaScript 實際上是功能強大、表現力強而且非常簡練的語言。它甚至具有其他更流行的語言才剛剛開始支援的功能。
JavaScript 的更進階功能之一是它支援閉包,這是 C# 2.0 通過它的匿名方法支援的功能。閉包是當内部函數(或 C# 中的内部匿名方法)綁定到它的外部函數的本地變量時所發生的運作時現象。很明顯,除非此内部函數以某種方式可被外部函數通路,否則它沒有多少意義。示例可 以更好說明這一點。
假設需要根據一個簡單條件篩選一個數字序列,這個條件是:隻有大于 100 的數字才能通過篩選,并忽略其餘數字。為此,可以編寫類似圖 8 中的函數。
Figure 8 根據謂詞篩選元素
function filter(pred, arr) {
var len = arr.length;
var filtered = []; // shorter version of new Array();
// iterate through every element in the array...
for(var i = 0; i < len; i++) {
var val = arr[i];
// if the element satisfies the predicate let it through
if(pred(val)) {
filtered.push(val);
}
}
return filtered;
}
var someRandomNumbers = [12, 32, 1, 3, 2, 2, 234, 236, 632,7, 8];
var numbersGreaterThan100 = filter(
function(x) { return (x > 100) ? true : false; },
someRandomNumbers);
// displays 234, 236, 632
alert(numbersGreaterThan100);
但是,現在要建立不同的篩選條件,假設這次隻有大于 300 的數字才能通過篩選,則可以編寫下面這樣的函數:
var greaterThan300 = filter(
function(x) { return (x > 300) ? true : false; },
someRandomNumbers);
然後,也許需要篩選大于 50 、25 、10 、600 如此等等的數字,但作為一個聰明人,您會發現它們全部都有相同的謂詞“greater than” ,隻有數字不同。是以,可以用類似下面的函數分開各個數字:
function makeGreaterThanPredicate(lowerBound) {
return function(numberToCheck) {
return (numberToCheck > lowerBound) ? true : false;
};
}
這樣,您就可以編寫以下代碼:
var greaterThan10 = makeGreaterThanPredicate(10);
var greaterThan100 = makeGreaterThanPredicate(100);
alert(filter(greaterThan10, someRandomNumbers));
alert(filter(greaterThan100, someRandomNumbers));
通 過觀察函數 makeGreaterThanPredicate 傳回的内部匿名函數,可以發現,該匿名内部函數使用 lowerBound ,後者是傳遞給 makeGreaterThanPredicate 的參數。按照作用域的一般規則,當 makeGreaterThanPredicate 退出時,lowerBound 超出了作用域!但在這裡,内部匿名函數仍然攜帶 lowerBound ,甚至在 makeGreaterThanPredicate 退出之後的很長時間内仍然如此。這就是我們所說的閉包:因為内部函數關閉了定義它的環境(即外部函數的參數和本地變量)。
開始可能感覺不到閉包的功能很強大。但如果應用恰當,它們就可以非常有創造性地幫您将想法轉換成代碼,這個過程非常有趣。在 JavaScript 中,閉包最有趣的用途之一是模拟類的私有變量。
模拟私有屬性
現 在介紹閉包如何幫助模拟私有成員。正常情況下,無法從函數以外通路函數内的本地變量。函數退出之後,由于各種實際原因,該本地變量将永遠消失。但是,如果 該本地變量被内部函數的閉包捕獲,它就會生存下來。這一事實是模拟 JavaScript 私有屬性的關鍵。假設有一個 Person 類:
function Person(name, age) {
this.getName = function() { return name; };
this.setName = function(newName) { name = newName; };
this.getAge = function() { return age; };
this.setAge = function(newAge) { age = newAge; };
}
參 數 name 和 age 是構造函數 Person 的本地變量。Person 傳回時,name 和 age 應當永遠消失。但是,它們被作為 Person 執行個體的方法而配置設定的四個内部函數捕獲,實際上這會使 name 和 age 繼續存在,但隻能嚴格地通過這四個方法通路它們。是以,您可以:
var ray = new Person(“Ray”, 31);
alert(ray.getName());
alert(ray.getAge());
ray.setName(“Younger Ray”);
// Instant rejuvenation!
ray.setAge(22);
alert(ray.getName() + “ is now “ + ray.getAge() +
“ years old.”);
未在構造函數中初始化的私有成員可以成為構造函數的本地變量,如下所示:
function Person(name, age) {
var occupation;
this.getOccupation = function() { return occupation; };
this.setOccupation = function(newOcc) { occupation =
newOcc; };
// accessors for name and age
}
注意,這些私有成員與我們期望從 C# 中産生的私有成員略有不同。在 C# 中,類的公用方法可以通路它的私有成員。但在 JavaScript 中,隻能通過在其閉包内擁有這些私有成員的方法來通路私有成員(由于這些方法不同于普通的公用方法,它們通常被稱為特權方法)。是以,在 Person 的公用方法中,仍然必須通過私有成員的特權通路器方法才能通路私有成員:
Person.prototype.somePublicMethod = function() {
// doesn’t work!
// alert(this.name);
// this one below works
alert(this.getName());
};
Douglas Crockford 是著名的發現(或者也許是釋出)使用閉包來模拟私有成員這一技術的第一人。他的網站 javascript.crockford.com 包含有關 JavaScript 的豐富資訊,任何對 JavaScript 感興趣的開發人員都應當仔細研讀。
從類繼承
到 這裡,我們已經了解了構造函數和原型對象如何使您在 JavaScript 中模拟類。您已經看到,原型鍊可以確定所有對象都有 Object.prototype 的公用方法,以及如何使用閉包來模拟類的私有成員。但這裡還缺少點什麼。您尚未看到如何從類派生,這在 C# 中是每天必做的工作。遺憾的是,在 JavaScript 中從類繼承并非像在 C# 中鍵入冒号即可繼承那樣簡單,它需要進行更多操作。另一方面,JavaScript 非常靈活,可以有很多從類繼承的方式。
例如,有一個基類 Pet ,它有一個派生類 Dog ,如圖 9 所示。這個在 JavaScript 中如何實作呢?Pet 類很容易。您已經看見如何實作它了:
圖 9 類
// class Pet
function Pet(name) {
this.getName = function() { return name; };
this.setName = function(newName) { name = newName; };
}
Pet.prototype.toString = function() {
return “This pet’s name is: “ + this.getName();
};
// end of class Pet
var parrotty = new Pet(“Parrotty the Parrot”);
alert(parrotty);
現在,如何建立從 Pet 派生的類 Dog 呢?在圖 9 中可以看到,Dog 有另一個屬性 breed ,它改寫了 Pet 的 toString 方法(注意,JavaScript 的約定是方法和屬性名稱使用 camel 大小寫,而不是在 C# 中建議的 Pascal 大小寫)。圖 10 顯示如何這樣做。
Figure 10 從 Pet 類派生
// class Dog : Pet
// public Dog(string name, string breed)
function Dog(name, breed) {
// think Dog : base(name)
Pet.call(this, name);
this.getBreed = function() { return breed; };
// Breed doesn’t change, obviously! It’s read only.
// this.setBreed = function(newBreed) { name = newName; };
}
// this makes Dog.prototype inherits
// from Pet.prototype
Dog.prototype = new Pet();
// remember that Pet.prototype.constructor
// points to Pet. We want our Dog instances’
// constructor to point to Dog.
Dog.prototype.constructor = Dog;
// Now we override Pet.prototype.toString
Dog.prototype.toString = function() {
return “This dog’s name is: “ + this.getName() +
“, and its breed is: “ + this.getBreed();
};
// end of class Dog
var dog = new Dog(“Buddy”, “Great Dane”);
// test the new toString()
alert(dog);
// Testing instanceof (similar to the is operator)
// (dog is Dog)? yes
alert(dog instanceof Dog);
// (dog is Pet)? yes
alert(dog instanceof Pet);
// (dog is Object)? yes
alert(dog instanceof Object);
所使用的原型 — 替換技巧正确設定了原型鍊,是以假如使用 C# ,測試的執行個體将按預期運作。而且,特權方法仍然會按預期運作。
模拟命名空間
在 C++ 和 C# 中,命名空間用于盡可能地減少名稱沖突。例如,在 .NET Framework 中,命名空間有助于将 Microsoft.Build.Task.Message 類與 System.Messaging.Message 區分開來。JavaScript 沒有任何特定語言功能來支援命名空間,但很容易使用對象來模拟命名空間。如果要建立一個 JavaScript 庫,則可以将它們包裝在命名空間内,而不需要定義全局函數和類,如下所示:
var MSDNMagNS = {};
MSDNMagNS.Pet = function(name) { // code here };
MSDNMagNS.Pet.prototype.toString = function() { // code };
var pet = new MSDNMagNS.Pet(“Yammer”);
命名空間的一個級别可能不是唯一的,是以可以建立嵌套的命名空間:
var MSDNMagNS = {};
// nested namespace “Examples”
MSDNMagNS.Examples = {};
MSDNMagNS.Examples.Pet = function(name) { // code };
MSDNMagNS.Examples.Pet.prototype.toString = function() { // code };
var pet = new MSDNMagNS.Examples.Pet(“Yammer”);
可以想象,鍵入這些冗長的嵌套命名空間會讓人很累。 幸運的是,庫使用者可以很容易地為命名空間指定更短的别名:
// MSDNMagNS.Examples and Pet definition...
// think “using Eg = MSDNMagNS.Examples;”
var Eg = MSDNMagNS.Examples;
var pet = new Eg.Pet(“Yammer”);
alert(pet);
如果看一下 Microsoft AJAX 庫的源代碼,就會發現庫的作者使用了類似的技術來實作命名空間(請參閱靜态方法 Type.registerNamespace 的實作)。有關詳細資訊,請參與側欄“OOP 和 ASP.NET AJAX” 。
應當這樣編寫 JavaScript 代碼嗎?
您 已經看見 JavaScript 可以很好地支援面向對象的程式設計。盡管它是一種基于原型的語言,但它的靈活性和強大功能可以滿足在其他流行語言中常見的基于類的程式設計風格。但問題是:是否應 當這樣編寫 JavaScript 代碼?在 JavaScript 中的程式設計方式是否應與 C# 或 C++ 中的編碼方式相同?是否有更聰明的方式來模拟 JavaScript 中沒有的功能?每種程式設計語言都各不相同,一種語言的最佳做法,對另一種語言而言則可能并非最佳。
在 JavaScript 中,您已看到對象繼承對象(與類繼承類不同)。是以,使用靜态繼承層次結建構立很多類的方式可能并不适合 JavaScript 。也許,就像 Douglas Crockford 在他的文章 Prototypal Inheritance in JavaScript 中說的那樣,JavaScript 程式設計方式是建立原型對象,并使用下面的簡單對象函數建立新的對象,而後者則繼承原始對象:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
然後,由于 JavaScript 中的對象是可延展的,是以可以友善地在建立對象之後,根據需要用新字段和新方法增大對象。
這 的确很好,但它不可否認的是,全世界大多數開發人員更熟悉基于類的程式設計。實際上,基于類的程式設計也會在這裡出現。按照即将頒發的 ECMA-262 規範第 4 版(ECMA-262 是 JavaScript 的官方規範),JavaScript 2.0 将擁有真正的類。是以,JavaScript 正在發展成為基于類的語言。但是,數年之後 JavaScript 2.0 才可能會被廣泛使用。同時,必須清楚目前的 JavaScript 完全可以用基于原型的風格和基于類的風格讀取和寫入 JavaScript 代碼。
展望
随 着互動式胖用戶端 AJAX 應用程式的廣泛使用,JavaScript 迅速成為 .NET 開發人員最重要的工具之一。但是,它的原型性質可能一開始會讓更習慣諸如 C++ 、C# 或 Visual Basic 等語言的開發人員感到吃驚。我已發現我的 JavaScript 學習經曆給予了我豐富的體驗,雖然其中也有一些挫折。如果本文能使您的體驗更加順利,我會非常高興,因為這正是我的目标。
OOP 和 ASP.NET AJAX
在 ASP.NET AJAX 中實作的 OOP 與在本文中讨論的規範實作稍有不同。這主要有兩個原因:ASP.NET AJAX 版本提供了更多反射可能性(它是諸如 xml 腳本等的聲明性文法和參數驗證所必需的),而且 ASP.NET AJAX 的目标是将使用 .NET 的開發人員所熟悉的某些其他構造(例如屬性、事件、枚舉和接口)轉換成 JavaScript 。
在 JavaScript 目前廣泛使用的版本中,它缺少 .NET 開發人員所熟悉的幾個 OOP 的關鍵概念,而 ASP.NET AJAX 可以模拟其中的大多數。
根 據命名約定(要遵守的示例),類可以有屬性通路器,以及多點傳播事件(符合緊密反映由 .NET 提供的約定的模式)。私有變量遵守成員以下劃線開頭則為私有的約定。很少有機會用到真正的私有變量,此政策是為了使調試程式能夠檢測到這些變量。引入接口 也是為了使類型檢查能夠避免常見的鴨子定型法(一種類型方案,它基于的概念是:如果有什麼物體走路和叫聲像鴨子,那麼它就是鴨子,或至少可以将它視為鴨 子)。
類和反射
在 JavaScript 中,沒有辦法知道函數的名稱。即使這是可能的,但在大多數情況下也沒有什麼用,因為類構造函數通常是通過向命名空間變量配置設定匿名函數來構造的。實際構成類 型名稱的是此變量的完全限定名稱,它同樣不可通路,并且構造函數本身對它一無所知。為了規避此限制,并使 JavaScript 類有豐富的反射,ASP.NET AJAX 需要将類型名稱進行注冊。
ASP.NET AJAX 中的反射 API 将檢查所有類型(無論是内置類型、類、接口、命名空間、或者甚至是枚舉),而它們包括的類似 .NET Framework 的函數(例如 isInstanceOfType 和 inheritsFrom )可以在運作時檢查類的層次結構。ASP.NET AJAX 還會在調試模式下執行某些類型檢查,這對開發人員更早捕獲 Bug 很有幫助。
注冊類層次結構和調用基礎函數
若要在 ASP.NET AJAX 中定義類,您需要将其構造函數賦給變量(注意,構造函數如何調用基礎函數):
MyNamespace.MyClass = function() {
MyNamespace.MyClass.initializeBase(this);
this._myProperty = null;
}
Then, you need to define the class members itself in its prototype:
MyNamespace.MyClass.prototype = {
get_myProperty: function() { return this._myProperty;},
set_myProperty: function(value) { this._myProperty = value; },
doSomething: function() {
MyNamespace.MyClass.callBaseMethod(this, “doSomething”);
}
}
最終注冊類:
MyNamespace.MyClass.registerClass(
“MyNamespace.MyClass “, MyNamespace.BaseClass);
此處不需要管理構造函數和原型層次結構,因為這由 registerClass 函數自動完成的。