轉自:http://msdn.microsoft.com/zh-tw/magazine/cc163419.aspx
最近我與一位擁有五年開發 Web 應用程式經驗的軟體開發人員面談。她使用 JavaScript 的經驗已長達四年半的時間,對自己的 JavaScript 技巧有很高的評價,但後來我很快發現,她其實對 JavaScript 一知半解。不過,我並沒有因此而責怪她。這就是 JavaScript 令人意想不到的所在。許多使用者 (直到最近,包括我自己在內喔) 都以為自己很懂得這個語言,只因為他們知道 C/C++/C# 或之前已有一些程式設計經驗。
從某方面來說,這種假設並非完全毫無根據。使用 JavaScript 設計一些簡單的程式很容易。它的學習門檻很低;這個語言比較容許失誤,您不需要深入了解這個語言就可以使用它來設計程式。即使非程式設計師也可以在幾小時內上手,撰寫一些對首頁有用的指令碼。
事實上,直到最近我才發現,我一直靠著對 JavaScript 貧乏的認知,憑藉著 MSDN® DHTML 參考手冊和我的 C++/C# 使用經驗,在勉強應付著。直到我開始設計真正的 AJAX 應用程式之後,才了解自己是如此欠缺 JavaScript 技巧。新一代 Web 應用程式的複雜性和互動性,需要以完全不同的方法來撰寫 JavaScript 程式碼。這些需要真正的 JavaScript 應用程式功力!我們一直以來所撰寫的用完即丟指令碼已經不夠。
物件導向程式設計 (OOP) 是許多 JavaScript 程式庫常用的方法之一,使程式碼基底更容易管理及維護。JavaScript 支援 OOP,但它支援的方式與一般符合 Microsoft® .NET Framework 規格的語言 (如 C++、C# 或 Visual Basic®) 支援的方式大不相同,因此,已長久使用那些語言工作的開發人員,一開始會覺得使用 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!
在 displayFullName 函數內使用 "this" 關鍵字,對於我們這種 C++/C# 開發人員應該不陌生 -- 它指向用來呼叫此方法的物件 (使用 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,它會以第一個引數的方法來呼叫該函數。也就是說,我們以第一個引數傳入呼叫的任何物件,將成為函數呼叫的 "this" 值。這將是呼叫基底類別建構函式的技巧,這點我們稍後會做說明。
請記住,絕對不要呼叫只包含 "this" 卻沒有主控物件的函數。如果您這麼做,將踐踏全域命名空間,因為在該呼叫中,"this" 將指向 Global 物件,而這樣會在應用程式中造成嚴重破壞。例如,以下的指令碼會變更 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 類別的執行個體。但是在 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 方法,這樣做很浪費,因為您只需要一個 respondTo 執行個體在 Dog 執行個體之間共用而已!如下所示,您可以在 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 屬性
現在,使用一個函數 (如上面範例中的 Dog) 來建立具有 "new" 運算子的物件時,產生的物件將繼承 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 原型鏈中的 Resolving toString() 方法 (按影像可放大)
JavaScript 動態解決屬性存取和方法呼叫的方式,會產生一些後果:
- 繼承的物件會立即看到對原型物件所做的變更,即使變更是發生在已建立繼承的物件之後。
- 如果您在物件中定義屬性/方法 X,相同名稱的屬性/方法將隱藏在該物件的原型中。例如,您可以在 Dog.prototype 中定義 toString 方法,來覆寫 Object.prototype 的 toString 方法。
- 變更僅從原型到其衍生物件單向進行,而不會反向進行。
[圖 7] 說明這些後果。[圖 7] 也顯示如何解決先前遇到的非必要方法之執行個體的問題。與其為每一個物件使用個別的函數物件執行個體,您可以改為將方法放到原型內,讓所有物件共用該方法即可。在此範例中,getBreed 方法會由 rover 和 spot 共用 -- 至少直到您覆寫 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# 中使用的語法幾乎一樣。因為建構函式實際上就是類別的名稱,所以並不顯得奇怪。現在您已經有類別了,也有公用屬性/方法和靜態屬性/方法。還需要些什麼呢?當然,還需要 Private 成員。但 JavaScript 沒有 Private 成員的原生支援 (也沒有 Protected 成員)。任何人都可以存取物件的所有屬性和方法。有一個方法可以讓您在類別中擁有 Private 成員,但在這麼做之前,您必須先了解 Closure。
Closure
我並非自願要學 JavaScript 的。我必須很快學會,否則我將無法應付真正的 AJAX 應用程式。一開始,我覺得自己的程式設計師等級往下掉了好幾級 (JavaScript!我的 C++ 朋友們會怎麼說呢?)但一旦克服最初的抗拒心之後,我才了解 JavaScript 其實是一個功能強大、表達性強而且精簡的語言。它甚至早已擁有其他更受歡迎的語言才開始要支援的功能呢。
JavaScript 其中一個較先進的功能,是 Closure 的支援,而 C# 2.0 是透過匿名方法支援 Closure。Closure 是一個執行階段現象,當內部函數 (在 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 等等的數字,但聰明的您知道它們全部使用相同的述詞,即「大於」。只是數字不同而已。因此,您可以使用如下的函數,將數字的設定挪出:
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 結束很久之後也一樣。這就是我們所謂的 Closure -- 因為內部函數會比定義該函數的環境 (也就是外部函數的引數和區域變數) 更早結束。
Closure 一開始看起來可能沒有什麼了不起。但若適當地使用,它們會顯露一些有趣的技巧,可以讓您實現更有創意的程式碼。在 JavaScript 中,Closure 最有趣的其中一個用途,就是模擬類別的私用變數。
模擬私用屬性
那麼,我們來看看 Closure 如何協助模擬 Private 成員。函數中的區域變數,一般無法從函數之外存取。在函數結束之後,實際上,區域變數就不見了。不過,內部函數 Closure 擷取的區域變數,會持續存活。這個事實就是模擬 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.”);
在建構函式中未初始化的 Private 成員可以是建構函式的區域變數,如下所示:
function Person(name, age) { var occupation; this.getOccupation = function() { return occupation; }; this.setOccupation = function(newOcc) { occupation = newOcc; }; // accessors for name and age }
請注意,這些 Private 成員與我們預期的 C# Private 成員稍微不同。在 C# 中,類別的公用方法可存取其 Private 成員。但是在 JavaScript 中,Private 成員只能透過使這些 Private 成員包含在 Closure 內的方法加以存取 (這些方法通常叫做權限方法 (Privileged Method),因為它們與一般公用方法不同)。因此在 Person 的公用方法內,您仍然必須透過特權存取子方法來存取 Private 成員:
Person.prototype.somePublicMethod = function() { // doesn’t work! // alert(this.name); // this one below works alert(this.getName()); };
著名的 Douglas Crockford 是第一位發現 (或者發佈) 使用 Closure 技巧來模擬 Private 成員的人。他的網站
javascript.crockford.com
有提供關於 JavaScript 的大量資訊 -- 任何對 JavaScript 感興趣的開發人員都應該進去看看。
繼承類別
現在,您已了解建構函式和原型物件如何讓您在 JavaScript 中模擬類別。您已了解到原型鏈可確保所有物件都有 Object.prototype 的共同方法。您也了解到如何使用 Closure 來模擬類別的 Private 成員。但這裡漏了一件事。您還不知道如何從類別衍生;這可是 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#,instanceof 測試將會如所預期般運作。同時,權限方法仍會如所預期般運作。
模擬命名空間
在 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 Library 的原始碼,會發現程式庫的作者使用類似技巧來實作命名空間 (請看看靜態方法 Type.registerNamespace 的實作)。如需詳細資訊,請參閱資訊看板<OOP 和 ASP.NET AJAX>。
您應該使用此方式來編寫 JavaScript 程式碼嗎?
您已經看到 JavaScript 對物件導向程式設計可提供很好的支援。雖然 JavaScript 採取原型語言的設計,但它有足夠的彈性和能力,可以呈現類別型程式設計風格,如同其他常見語言。問題是:您應該使用此方式來編寫 JavaScript 程式碼嗎?您要以 C# 或 C++ 編寫程式碼的方式來編寫 JavaScript 程式碼嗎?亦即以更聰明的技巧來模擬本身沒有的功能?每一種程式設計語言都有所不同,一種語言的最佳作法不一定是另一種語言的最佳作法。
您已看到 JavaScript 中的物件會繼承其他物件 (相對於類別繼承類別)。因此,使用靜態繼承階層來建立許多類別,或許不是 JavaScript 的最佳運用。也許就像 Douglas Crockford 在他的文章 (
JavaScript 的原型繼承關係 (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 學習過程是一段很寶貴的經驗。如果本文可以幫助您更順利學習,我就感到很欣慰了,這也是我的目標。