最近,我面試了一個有五年 Web 應用程序開發(fā)經(jīng)驗的軟件開發(fā)人員。四年半來她一直在從事 JavaScript 相關的工作,她自認為 JavaScript 技能非常好,但在不久之后我就發(fā)現(xiàn)實際上她對 JavaScript 知之甚少。話雖這樣說,但我確實沒有責備她的意思。JavaScript 真的是很有趣。很多人(包括我自己,直到最近?。┒颊J為自己很擅長 JavaScript 語言,因為他們都知道 C/C++/C#,或者有一些以前的編程經(jīng)驗。
在某種程度上,這種假設并不是完全沒有根據(jù)的。用 JavaScript 很容易做些簡單的事情。入門的門檻很低,該語言很寬松,它不需要您知道很多細節(jié)就可以開始用它進行編碼。甚至非編程人員也可能用它在幾個小時內(nèi)為主頁編寫一些有用的腳本。
的確,直到最近,僅僅憑借 MSDN® DHTML 參考資料和我的 C++/C# 經(jīng)驗,我也總能勉強利用這點 JavaScript 知識完成一些任務。只是當我開始編寫真實的 AJAX 應用程序時,我才意識到實際上我的 JavaScript 知識還非常不夠。這個新一代的 Web 應用程序的復雜性和交互性需要程序員以完全不同的方法來編寫 JavaScript 代碼。它們是真正的 JavaScript 應用程序!我們在編寫一次性腳本時一直采用的方法已完全不再有效。
面向?qū)ο缶幊?(OOP) 是一種流行的編程方法,很多 JavaScript 庫中都使用這種方法,以便更好地管理和維護基本代碼。JavaScript 支持 OOP,但與諸如 C++、C# 或 Visual Basic® 等流行的 Microsoft® .NET Framework 兼容語言相比,它支持 OOP 的方式非常不同,因此主要使用這些語言的開發(fā)人員開始可能會覺得在 JavaScript 中使用 OOP 很奇怪而且不直觀。我寫本文就是為了深入討論 JavaScript 語言實際上如何支持面向?qū)ο缶幊?,以及您如何使用這一支持在 JavaScript 中高效地進行面向?qū)ο箝_發(fā)。下面首先討論對象(還能先討論其他別的什么呢?)。
JavaScript 對象是詞典
在 C++ 或 C# 中,在談論對象時,是指類或結(jié)構(gòu)的實例。對象有不同的屬性和方法,具體取決于將它們實例化的模板(即類)。而 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 的人會發(fā)現(xiàn)在第二和第三個代碼段中實例化 userObject 的方法與在 Python 中指定詞典的方法完全相同。唯一的差異是 JavaScript 對象/詞典只接受字符串關鍵字,而不是像 Python 詞典那樣接受可哈?;膶ο?。
這些示例還顯示 JavaScript 對象比 C++ 或 C# 對象具有更大的可延展性。您不必預先聲明屬性 lastLoginTime — 如果 userObject 沒有該名稱的屬性,該屬性將被直接添加到 userObject。如果記住 JavaScript 對象是詞典,您就不會對此感到吃驚了,畢竟,我們一直在向詞典添加新關鍵字(和其各自的值)。
這樣,我們就有了對象屬性。對象方法呢?同樣,JavaScript 與 C++/C# 不同。若要理解對象方法,首先需要仔細了解一下 JavaScript 函數(shù)。
JavaScript 函數(shù)是最棒的
在很多編程語言中,函數(shù)和對象通常被視為兩樣不同的東西。在 JavaScript 中,其差別很模糊 — JavaScript 函數(shù)實際上是具有與它關聯(lián)的可執(zhí)行代碼的對象。請如此看待普通函數(shù):
|
function func(x) { alert(x); } func(“blah”); |
這就是通常在 JavaScript 中定義函數(shù)的方法。但是,還可以按以下方法定義該函數(shù),您在此創(chuàng)建匿名函數(shù)對象,并將它賦給變量 func
|
var func = function(x) { alert(x); }; func(“blah2”); |
甚至也可以像下面這樣,使用 Function 構(gòu)造函數(shù):
|
var func = new Function(“x”, “alert(x);”); func(“blah3”); |
此示例表明函數(shù)實際上只是支持函數(shù)調(diào)用操作的對象。最后一個使用 Function 構(gòu)造函數(shù)來定義函數(shù)的方法并不常用,但它展示的可能性非常有趣,因為您可能注意到,該函數(shù)的主體正是 Function 構(gòu)造函數(shù)的 String 參數(shù)。這意味著,您可以在運行時構(gòu)造任意函數(shù)。
為了進一步演示函數(shù)是對象,您可以像對其他任何 JavaScript 對象一樣,在函數(shù)中設置或添加屬性:
|
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.” |
作為對象,函數(shù)還可以賦給變量、作為參數(shù)傳遞給其他函數(shù)、作為其他函數(shù)的值返回,并可以作為對象的屬性或數(shù)組的元素進行存儲等等。圖 1 提供了這樣一個示例。
Figure 1 JavaScript 中的函數(shù)是最棒的
|
// 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); |
記住這一點后,向?qū)ο筇砑臃椒▽⑹呛苋菀椎氖虑椋褐恍柽x擇名稱,然后將函數(shù)賦給該名稱。因此,我通過將匿名函數(shù)分別賦給相應的方法名稱,在對象中定義了三個方法:
|
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# 開發(fā)人員應當很熟悉 displayFullName 函數(shù)中使用的“this”關鍵字 — 它引用一個對象,通過對象調(diào)用方法(使用 Visual Basic 的開發(fā)人員也應當很熟悉它,它在 Visual Basic 中叫做“Me”)。因此在上面的示例中,displayFullName 中的“this”的值是 myDog 對象。但是,“this”的值不是靜態(tài)的。通過不同對象調(diào)用“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 中的最后一行表示的是將函數(shù)作為對象的方法進行調(diào)用的另一種方式。請記住,JavaScript 中的函數(shù)是對象。每個函數(shù)對象都有一個名為 call 的方法,它將函數(shù)作為第一個參數(shù)的方法進行調(diào)用。就是說,作為函數(shù)第一個參數(shù)傳遞給 call 的任何對象都將在函數(shù)調(diào)用中成為“this”的值。這一技術對于調(diào)用基類構(gòu)造函數(shù)來說非常有用,稍后將對此進行介紹。
有一點需要記住,絕不要調(diào)用包含“this”(卻沒有所屬對象)的函數(shù)。否則,將違反全局命名空間,因為在該調(diào)用中,“this”將引用全局對象,而這必然會給您的應用程序帶來災難。例如,下面的腳本將更改 JavaScript 的全局函數(shù) 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)); |
到這里,我們已經(jīng)介紹了如何創(chuàng)建對象,包括它的屬性和方法。但如果注意上面的所有代碼段,您會發(fā)現(xiàn)屬性和方法是在對象定義本身中進行硬編碼的。但如果需要更好地控制對象的創(chuàng)建,該怎么做呢?例如,您可能需要根據(jù)某些參數(shù)來計算對象的屬性值?;蛘撸赡苄枰獙ο蟮膶傩猿跏蓟癁閮H在運行時才能獲得的值。也可能需要創(chuàng)建對象的多個實例(此要求非常常見)。
在 C# 中,我們使用類來實例化對象實例。但 JavaScript 與此不同,因為它沒有類。您將在下一節(jié)中看到,您可以充分利用這一情況:函數(shù)在與“new”運算符一起使用時,函數(shù)將充當構(gòu)造函數(shù)。
構(gòu)造函數(shù)而不是類
前面提到過,有關 JavaScript OOP 的最奇怪的事情是,JavaScript 不像 C# 或 C++ 那樣,它沒有類。在 C# 中,在執(zhí)行類似下面的操作時:
將返回一個對象,該對象是 Dog 類的實例。但在 JavaScript 中,本來就沒有類。與訪問類最近似的方法是定義構(gòu)造函數(shù),如下所示:
|
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! |
那么,結(jié)果會怎樣呢?暫時忽略 DogConstructor 函數(shù)定義,看一看這一行:
|
var spot = new DogConstructor(“Spot”); |
“new”運算符執(zhí)行的操作很簡單。首先,它創(chuàng)建一個新的空對象。然后執(zhí)行緊隨其后的函數(shù)調(diào)用,將新的空對象設置為該函數(shù)中“this”的值。換句話說,可以認為上面這行包含“new”運算符的代碼與下面兩行代碼的功能相當:
|
// create an empty object var spot = {}; // call the function as a method of the empty object DogConstructor.call(spot, “Spot”); |
正如在 DogConstructor 主體中看到的那樣,調(diào)用此函數(shù)將初始化對象,在調(diào)用期間關鍵字“this”將引用此對象。這樣,就可以為對象創(chuàng)建模板!只要需要創(chuàng)建類似的對象,就可以與構(gòu)造函數(shù)一起調(diào)用“new”,返回的結(jié)果將是一個完全初始化的對象。這與類非常相似,不是嗎?實際上,在 JavaScript 中構(gòu)造函數(shù)的名稱通常就是所模擬的類的名稱,因此在上面的示例中,可以直接命名構(gòu)造函數(shù) 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 作為其構(gòu)造函數(shù)所創(chuàng)建的每個對象都有它自己的實例變量名稱副本(前面提到過,它就是對象詞典的條目)。這就是希望的結(jié)果。畢竟,每個對象都需要它自己的實例變量副本來表示其狀態(tài)。但如果看看下一行,就會發(fā)現(xiàn)每個 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 實例(即用構(gòu)造函數(shù) Dog 創(chuàng)建的所有實例)都可以共享 respondTo 方法的一個實例。但隨著方法數(shù)的增加,維護工作將越來越難。最后,基本代碼中將有很多全局函數(shù),而且隨著“類”的增加,事情只會變得更加糟糕(如果它們的方法具有相似的名稱,則尤甚)。但使用原型對象可以更好地解決這個問題,這是下一節(jié)的主題。
原型
在使用 JavaScript 的面向?qū)ο缶幊讨?,原型對象是個核心概念。在 JavaScript 中對象是作為現(xiàn)有示例(即原型)對象的副本而創(chuàng)建的,該名稱就來自于這一概念。此原型對象的任何屬性和方法都將顯示為從原型的構(gòu)造函數(shù)創(chuàng)建的對象的屬性和方法??梢哉f,這些對象從其原型繼承了屬性和方法。當您創(chuàng)建如下所示的新 Dog 對象時:
|
var buddy = new Dog(“Buddy“); |
buddy 所引用的對象將從它的原型繼承屬性和方法,盡管僅從這一行可能無法明確判斷原型來自哪里。對象 buddy 的原型來自構(gòu)造函數(shù)(在這里是函數(shù) Dog)的屬性。
在 JavaScript 中,每個函數(shù)都有名為“prototype”的屬性,用于引用原型對象。此原型對象又有名為“constructor”的屬性,它反過來引用函數(shù)本身。這是一種循環(huán)引用,圖 3 更好地說明了這種循環(huán)關系。
圖 3 每個函數(shù)的原型都有一個 Constructor 屬性
現(xiàn)在,通過“new”運算符用函數(shù)(上面示例中為 Dog)創(chuàng)建對象時,所獲得的對象將繼承 Dog.prototype 的屬性。在圖 3 中,可以看到 Dog.prototype 對象有一個回指 Dog 函數(shù)的構(gòu)造函數(shù)屬性。這樣,每個 Dog 對象(從 Dog.prototype 繼承而來)都有一個回指 Dog 函數(shù)的構(gòu)造函數(shù)屬性。圖 4 中的代碼證實了這一點。圖 5 顯示了構(gòu)造函數(shù)、原型對象以及用它們創(chuàng)建的對象之間的這一關系。
|
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”)); |
某些讀者可能已經(jīng)注意到圖 4 中對 hasOwnProperty 和 isPrototypeOf 方法的調(diào)用。這些方法是從哪里來的呢?它們不是來自 Dog.prototype。實際上,在 Dog.prototype 和 Dog 實例中還可以調(diào)用其他方法,比如 toString、toLocaleString 和 valueOf,但它們都不來自 Dog.prototype。您會發(fā)現(xiàn),就像 .NET Framework 中的 System.Object 充當所有類的最終基類一樣,JavaScript 中的 Object.prototype 是所有原型的最終基礎原型。(Object.prototype 的原型是 null。)
在此示例中,請記住 Dog.prototype 是對象。它是通過調(diào)用 Object 構(gòu)造函數(shù)創(chuàng)建的(盡管它不可見):
|
Dog.prototype = new Object(); |
因此,正如 Dog 實例繼承 Dog.prototype 一樣,Dog.prototype 繼承 Object.prototype。這使得所有 Dog 實例也繼承了 Object.prototype 的方法和屬性。
每個 JavaScript 對象都繼承一個原型鏈,而所有原型都終止于 Object.prototype。注意,迄今為止您看到的這種繼承是活動對象之間的繼承。它不同于繼承的常見概念,后者是指在聲明類時類之間的發(fā)生的繼承。因此,JavaScript 繼承動態(tài)性更強。它使用簡單算法實現(xiàn)這一點,如下所示:當您嘗試訪問對象的屬性/方法時,JavaScript 將檢查該屬性/方法是否是在該對象中定義的。如果不是,則檢查對象的原型。如果還不是,則檢查該對象的原型的原型,如此繼續(xù),一直檢查到 Object.prototype。圖 6 說明了此解析過程。
圖 6 在原型鏈中解析 toString() 方法 (單擊該圖像獲得較大視圖)
JavaScript 動態(tài)地解析屬性訪問和方法調(diào)用的方式產(chǎn)生了一些特殊效果:
- 繼承原型對象的對象上可以立即呈現(xiàn)對原型所做的更改,即使是在創(chuàng)建這些對象之后。
- 如果在對象中定義了屬性/方法 X,則該對象的原型中將隱藏同名的屬性/方法。例如,通過在 Dog.prototype 中定義 toString 方法,可以改寫 Object.prototype 的 toString 方法。
- 更改只沿一個方向傳遞,即從原型到它的派生對象,但不能沿相反方向傳遞。
圖 7 說明了這些效果。圖 7 還顯示了如何解決前面遇到的不需要的方法實例的問題。通過將方法放在原型內(nèi)部,可以使對象共享方法,而不必使每個對象都有單獨的函數(shù)對象實例。在此示例中,rover 和 spot 共享 getBreed 方法,直至在 spot 中以任何方式改寫 toString 方法。此后,spot 有了它自己版本的 getBreed 方法,但 rover 對象和用新 GreatDane 創(chuàng)建的后續(xù)對象仍將共享在 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()); |
靜態(tài)屬性和方法
有時,您需要綁定到類而不是實例的屬性或方法,也就是,靜態(tài)屬性和方法。在 JavaScript 中很容易做到這一點,因為函數(shù)是可以按需要設置其屬性和方法的對象。由于在 JavaScript 中構(gòu)造函數(shù)表示類,因此可以通過在構(gòu)造函數(shù)中設置靜態(tài)方法和屬性,直接將它們添加到類中,如下所示:
|
function DateTime() { } // set static method now() DateTime.now = function() { return new Date(); }; alert(DateTime.now()); |
在 JavaScript 中調(diào)用靜態(tài)方法的語法與在 C# 中幾乎完全相同。這不應當讓人感到吃驚,因為構(gòu)造函數(shù)的名稱實際上是類的名稱。這樣,就有了類、公用屬性/方法,以及靜態(tài)屬性/方法。還需要其他什么嗎?當然,私有成員。但 JavaScript 本身并不支持私有成員(同樣,也不支持受保護成員)。任何人都可以訪問對象的所有屬性和方法。但我們有辦法讓類中包含私有成員,但在此之前,您首先需要理解閉包。
閉包
我沒有自覺地學習過 JavaScript。我必須快點了解它,因為我發(fā)現(xiàn)如果沒有它,在實際工作中編寫 AJAX 應用程序的準備就會不充分。開始,我感到我的編程水平好像降了幾個級別。(JavaScript!我的 C++ 朋友會怎么說?)但一旦我克服最初的障礙,我就發(fā)現(xiàn) JavaScript 實際上是功能強大、表現(xiàn)力強而且非常簡練的語言。它甚至具有其他更流行的語言才剛剛開始支持的功能。
JavaScript 的更高級功能之一是它支持閉包,這是 C# 2.0 通過它的匿名方法支持的功能。閉包是當內(nèi)部函數(shù)(或 C# 中的內(nèi)部匿名方法)綁定到它的外部函數(shù)的本地變量時所發(fā)生的運行時現(xiàn)象。很明顯,除非此內(nèi)部函數(shù)以某種方式可被外部函數(shù)訪問,否則它沒有多少意義。示例可以更好說明這一點。
假設需要根據(jù)一個簡單條件篩選一個數(shù)字序列,這個條件是:只有大于 100 的數(shù)字才能通過篩選,并忽略其余數(shù)字。為此,可以編寫類似圖 8 中的函數(shù)。
|
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); |
但是,現(xiàn)在要創(chuàng)建不同的篩選條件,假設這次只有大于 300 的數(shù)字才能通過篩選,則可以編寫下面這樣的函數(shù):
|
var greaterThan300 = filter( function(x) { return (x > 300) ? true : false; }, someRandomNumbers); |
然后,也許需要篩選大于 50、25、10、600 如此等等的數(shù)字,但作為一個聰明人,您會發(fā)現(xiàn)它們?nèi)慷加邢嗤闹^詞“greater than”,只有數(shù)字不同。因此,可以用類似下面的函數(shù)分開各個數(shù)字:
|
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)); |
通過觀察函數(shù) makeGreaterThanPredicate 返回的內(nèi)部匿名函數(shù),可以發(fā)現(xiàn),該匿名內(nèi)部函數(shù)使用 lowerBound,后者是傳遞給 makeGreaterThanPredicate 的參數(shù)。按照作用域的一般規(guī)則,當 makeGreaterThanPredicate 退出時,lowerBound 超出了作用域!但在這里,內(nèi)部匿名函數(shù)仍然攜帶 lowerBound,甚至在 makeGreaterThanPredicate 退出之后的很長時間內(nèi)仍然如此。這就是我們所說的閉包:因為內(nèi)部函數(shù)關閉了定義它的環(huán)境(即外部函數(shù)的參數(shù)和本地變量)。
開始可能感覺不到閉包的功能很強大。但如果應用恰當,它們就可以非常有創(chuàng)造性地幫您將想法轉(zhuǎn)換成代碼,這個過程非常有趣。在 JavaScript 中,閉包最有趣的用途之一是模擬類的私有變量。
模擬私有屬性
現(xiàn)在介紹閉包如何幫助模擬私有成員。正常情況下,無法從函數(shù)以外訪問函數(shù)內(nèi)的本地變量。函數(shù)退出之后,由于各種實際原因,該本地變量將永遠消失。但是,如果該本地變量被內(nèi)部函數(shù)的閉包捕獲,它就會生存下來。這一事實是模擬 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; }; } |
參數(shù) name 和 age 是構(gòu)造函數(shù) Person 的本地變量。Person 返回時,name 和 age 應當永遠消失。但是,它們被作為 Person 實例的方法而分配的四個內(nèi)部函數(shù)捕獲,實際上這會使 name 和 age 繼續(xù)存在,但只能嚴格地通過這四個方法訪問它們。因此,您可以:
|
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.”); |
未在構(gòu)造函數(shù)中初始化的私有成員可以成為構(gòu)造函數(shù)的本地變量,如下所示:
|
function Person(name, age) { var occupation; this.getOccupation = function() { return occupation; }; this.setOccupation = function(newOcc) { occupation = newOcc; }; // accessors for name and age } |
注意,這些私有成員與我們期望從 C# 中產(chǎn)生的私有成員略有不同。在 C# 中,類的公用方法可以訪問它的私有成員。但在 JavaScript 中,只能通過在其閉包內(nèi)擁有這些私有成員的方法來訪問私有成員(由于這些方法不同于普通的公用方法,它們通常被稱為特權(quán)方法)。因此,在 Person 的公用方法中,仍然必須通過私有成員的特權(quán)訪問器方法才能訪問私有成員:
|
Person.prototype.somePublicMethod = function() { // doesn’t work! // alert(this.name); // this one below works alert(this.getName()); }; |
Douglas Crockford 是著名的發(fā)現(xiàn)(或者也許是發(fā)布)使用閉包來模擬私有成員這一技術的第一人。他的網(wǎng)站
javascript.crockford.com 包含有關 JavaScript 的豐富信息,任何對 JavaScript 感興趣的開發(fā)人員都應當仔細研讀。
從類繼承
到這里,我們已經(jīng)了解了構(gòu)造函數(shù)和原型對象如何使您在 JavaScript 中模擬類。您已經(jīng)看到,原型鏈可以確保所有對象都有 Object.prototype 的公用方法,以及如何使用閉包來模擬類的私有成員。但這里還缺少點什么。您尚未看到如何從類派生,這在 C# 中是每天必做的工作。遺憾的是,在 JavaScript 中從類繼承并非像在 C# 中鍵入冒號即可繼承那樣簡單,它需要進行更多操作。另一方面,JavaScript 非常靈活,可以有很多從類繼承的方式。
例如,有一個基類 Pet,它有一個派生類 Dog,如圖 9 所示。這個在 JavaScript 中如何實現(xiàn)呢?Pet 類很容易。您已經(jīng)看見如何實現(xiàn)它了:
|
// 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); |
現(xiàn)在,如何創(chuàng)建從 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#,測試的實例將按預期運行。而且,特權(quán)方法仍然會按預期運行。
模擬命名空間
在 C++ 和 C# 中,命名空間用于盡可能地減少名稱沖突。例如,在 .NET Framework 中,命名空間有助于將 Microsoft.Build.Task.Message 類與 System.Messaging.Message 區(qū)分開來。JavaScript 沒有任何特定語言功能來支持命名空間,但很容易使用對象來模擬命名空間。如果要創(chuàng)建一個 JavaScript 庫,則可以將它們包裝在命名空間內(nèi),而不需要定義全局函數(shù)和類,如下所示:
|
var MSDNMagNS = {}; MSDNMagNS.Pet = function(name) { // code here }; MSDNMagNS.Pet.prototype.toString = function() { // code }; var pet = new MSDNMagNS.Pet(“Yammer”); |
命名空間的一個級別可能不是唯一的,因此可以創(chuàng)建嵌套的命名空間:
|
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 庫的源代碼,就會發(fā)現(xiàn)庫的作者使用了類似的技術來實現(xiàn)命名空間(請參閱靜態(tài)方法 Type.registerNamespace 的實現(xiàn))。有關詳細信息,請參與側(cè)欄“OOP 和 ASP.NET AJAX”。
應當這樣編寫 JavaScript 代碼嗎?
您已經(jīng)看見 JavaScript 可以很好地支持面向?qū)ο蟮木幊獭1M管它是一種基于原型的語言,但它的靈活性和強大功能可以滿足在其他流行語言中常見的基于類的編程風格。但問題是:是否應當這樣編寫 JavaScript 代碼?在 JavaScript 中的編程方式是否應與 C# 或 C++ 中的編碼方式相同?是否有更聰明的方式來模擬 JavaScript 中沒有的功能?每種編程語言都各不相同,一種語言的最佳做法,對另一種語言而言則可能并非最佳。
在 JavaScript 中,您已看到對象繼承對象(與類繼承類不同)。因此,使用靜態(tài)繼承層次結(jié)構(gòu)建立很多類的方式可能并不適合 JavaScript。也許,就像 Douglas Crockford 在他的文章
Prototypal Inheritance in JavaScript 中說的那樣,JavaScript 編程方式是建立原型對象,并使用下面的簡單對象函數(shù)建立新的對象,而后者則繼承原始對象:
|
function object(o) { function F() {} F.prototype = o; return new F(); } |
然后,由于 JavaScript 中的對象是可延展的,因此可以方便地在創(chuàng)建對象之后,根據(jù)需要用新字段和新方法增大對象。
這的確很好,但它不可否認的是,全世界大多數(shù)開發(fā)人員更熟悉基于類的編程。實際上,基于類的編程也會在這里出現(xiàn)。按照即將頒發(fā)的 ECMA-262 規(guī)范第 4 版(ECMA-262 是 JavaScript 的官方規(guī)范),JavaScript 2.0 將擁有真正的類。因此,JavaScript 正在發(fā)展成為基于類的語言。但是,數(shù)年之后 JavaScript 2.0 才可能會被廣泛使用。同時,必須清楚當前的 JavaScript 完全可以用基于原型的風格和基于類的風格讀取和寫入 JavaScript 代碼。
展望
隨著交互式胖客戶端 AJAX 應用程序的廣泛使用,JavaScript 迅速成為 .NET 開發(fā)人員最重要的工具之一。但是,它的原型性質(zhì)可能一開始會讓更習慣諸如 C++、C# 或 Visual Basic 等語言的開發(fā)人員感到吃驚。我已發(fā)現(xiàn)我的 JavaScript 學習經(jīng)歷給予了我豐富的體驗,雖然其中也有一些挫折。如果本文能使您的體驗更加順利,我會非常高興,因為這正是我的目標