Menu

Javascript 物件導向式程式設計基礎講解

最近我與一位擁有五年開發 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] 提供這種範例。

// 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] 所示。
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 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 方法,這樣做很浪費,因為您只需要一個 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] 描繪建構函式、原型物件,以及它們所建立的物件之間的關係。
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 方法的執行個體。
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] 中的函數。
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] 顯示做法。
// 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 學習過程是一段很寶貴的經驗。如果本文可以幫助您更順利學習,我就感到很欣慰了,這也是我的目標。

OOP 和 ASP.NET AJAX

在 ASP.NET AJAX 實作的 OOP 與本文所討論的正規實作有點不同。關於這點,有兩個主要原因:ASP.NET AJAX 版本提供更多反映的可能性 (這對於 xml-script 之類的宣告語法和參數驗證是必要的),而 ASP.NET AJAX 的目標在於將 .NET 開發人員所熟悉的其他一些結構 (例如屬性、事件、列舉值和介面) 轉換至 JavaScript 中。

在目前廣泛使用的版本中,JavaScript 缺少 .NET 開發人員所熟悉的一些 OOP 主要概念,而 ASP.NET AJAX 則大部分採用模擬的方式。

類 別可以有根據命名慣例的屬性存取子 (後面有範例),以及模式類似 .NET 所提供的多點傳送事件。私用變數所依據的慣例,即以底線做為開頭的成員是 Private 成員。真正的私用變數很少會用到,此原則讓那些變數能夠由偵錯工具進行監督。其中也引進了介面,在一般 duck-typing 之外啟用型別檢查情況 (duck-typing 是一種型別,它所依據的概念是:如果有個東西走起路來像鴨子,叫起來也像鴨子,那麼它一定是隻鴨子,或至少可以把它當做是隻鴨子來看)。

類別和反映

在 JavaScript 中,您無從得知函數的名稱。即使有可能,在大部分情況下也沒有幫助,因為類別建構函式一般的建立方式,是將匿名函數指派至命名空間變數。真正構成型別名稱 的是此變數的完整名稱,它同樣也是無法存取的,而且建構函式本身對它一無所知。為了克服此限制,並對 JavaScript 類別有眾多反映,ASP.NET AJAX 要求登錄型別名稱。
在 ASP.NET AJAX 中,反映 API 可對任何型別運作,不論它是內建、類別、介面、命名空間、甚至是列舉值,而且它們包括可在執行階段監督類別階層的 .NET Framework 函數 (如 isInstanceOfType 和 inheritsFrom)。ASP.NET AJAX 也會在偵錯模式下執行一些型別檢查,這可幫助開發人員儘早捕捉到錯誤。

登錄類別階層和呼叫基底

若要在 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」);
/* do something more */
}
}
然後才登錄類別:
MyNamespace.MyClass.registerClass(
「MyNamespace.MyClass 「, MyNamespace.BaseClass);
您不需要管理建構函式和原型階層,因為 registerClass 函數會自動為您執行此動作。

Bertrand Le Roy 是 ASP.NET AJAX 小組的軟體設計工程師 (Software Design Engineer II)。