現 在,通過“new”運算符用函數(上面示例中為 Dog)創建對象時,所獲得的對象將繼承 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 在原型鏈中解析 toString() 方法 (單 擊該圖像獲得較大視圖)
JavaScript 動態地解析屬性訪問和方法調用的方式產生了一些特殊效果:
繼承原型對象的對象上可以立即呈現對原型所做的更改,即使是在創建這些對象之后。
如果在對象中定義了屬性/方法 X,則該對象的原型中將隱藏同名的屬性/方法。例如,通過在 Dog.prototype 中定義 toString 方法,可以改寫 Object.prototype 的 toString 方法。
更改只沿一個方向傳遞,即從原型到它的派生對象,但不能沿相反方向傳 遞。
圖 7 說明了這些效果。圖 7 還顯示了如何解決前面遇到的不需要的方法實例的問題。通過將方法放在原型內部,可以使對象共享方法,而不必使每個對象都有單獨的函數對象實例。在此示例 中,rover 和 spot 共享 getBreed 方法,直至在 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# 中幾乎完全相同。這不應當讓人感到吃驚,因為構造函數的名稱實際上是類的名稱。這樣,就有了類、公用屬性/方法,以及靜態屬性/方法。還需要其他什么嗎? 當然,私有成員。但 JavaScript 本身并不支持私有成員(同樣,也不支持受保護成員)。任何人都可以訪問對象的所有屬性和方法。但我們有辦法讓類中包含私有成員,但在此之前,您首先需要理 解閉包。
閉包
我 沒有自覺地學習過 JavaScript。我必須快點了解它,因為我發現如果沒有它,在實際工作中編寫 AJAX 應用程序的準備就會不充分。開始,我感到我的編程水平好像降了幾個級別。(JavaScript!我的 C++ 朋友會怎么說?)但一旦我克服最初的障礙,我就發現 JavaScript 實際上是功能強大、表現力強而且非常簡練的語言。它甚至具有其他更流行的語言才剛剛開始支持的功能。
JavaScript 的更高級功能之一是它支持閉包,這是 C# 2.0 通過它的匿名方法支持的功能。閉包是當內部函數(或 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 如此等等的數字,但作為一個聰明人,您會發現它們全部都有相同的謂詞“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 類很容易。您已經看見如何實現它了:
// 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 顯示如何這樣做。
從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 學習經歷給予了我豐富的體驗,雖然其中也有一些挫折。如果本文能使您的體驗更加順利,我會非常高興,因為這正是我的目標。