1. 程式人生 > >用面向物件技術建立先進的web應用

用面向物件技術建立先進的web應用

翻譯如下:

             使用面向物件技術建立先進的web應用程式

作者:Ray Djajadinata

     不久我面試了一位在開發web應用方面擁有五年程式設計經驗的開發者,她使用javascript已有4年半的時間的了,自認為在javascript方面擁有很強的技能。隨後我發現她對javascript所知甚少,但我並沒有因此。實際上,javascript就是這樣有趣,它很容易使人們產生自己很精通它的錯覺,僅僅是因為他們熟悉C/C++或C#語言,或憑著自己先前的程式設計經驗

      從某種程度上來說,該假設並不是毫無根據的。我們很容易使用javascript來做些簡單的事情。入門的門檻很低,你並不需要知道多少東西,就能夠開始用它編寫程式碼,甚至一個毫無程式設計經驗的人,就能夠通過幾個小時的學習為主頁寫一個有用的指令碼。


     實際上,我一直僅憑參考MSDN上關於DHTML的內容以及自己的C++/C#的程式設計經驗,利用自己對javascript的一知半解過活,當我在實際工作做有關AJAX應用的時候,我才意識的我對javascript理解是多麼膚淺。新的web應用複雜性以及互動性的特點要求我們需用完全不同的方法來編寫javascript程式碼。這些才是真正javascript應用程式。我們在編寫一次性指令碼時一直採用的方法已完全不再有效。

    為了使程式碼庫便於管理和維護,面向物件程式設計方式在許多javascript庫中非常流行。JavaScript支援面向物件程式設計,但跟Microsoft®.NET Framework中的C++/C#或Visual Basic 的實現方式有很大的不同。所以對那些一直使用這些語言的開發者來說,一開始在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 物件初始化化好相似。從程式碼二和程式碼三中,我們可以看出例項化userObject的方式與Pthyon語言中建立字典的語法好相似。

   唯一的不同點是,javascript 物件/字典只接受字元鍵,而不像python那樣接受雜湊物件。上面的例子也顯示出了javascript物件比C++/C#物件擁有更多的靈活性從程式碼一,程式碼二可以看出,lastLoginTime並不需要事先宣告,假如物件userObject沒有該屬性時,該屬性會被新增到userObject物件中。當你意識到javascript物件僅是一個字典,這看上去並不驚訝。我們可以在任何時間往字典中增加鍵值。我們現在知道怎麼去建立的物件的屬性了。那物件中的方法我們該怎麼去建立呢。javascript語言不同於C++/C#,為了能夠理解javascript物件中方法,我們需要更進一步的認識javascript中的函式。

JavaScript函式是第一類函式

 在許多程式語言中,函式和物件是無放在一起比較的。然而在javascript中,它們之間的概念很模糊,在javascript中函式是一個關聯可執行程式碼的物件。看看下面程式碼中的函式:

function func(x) {

    alert(x);

}

func("blah");
這是我們經常定義函式的方式,你也可以按如下的方式定義該函式,下面的方式定義個匿名函式,並把它賦給了變數func。
var func = function(x) {

    alert(x);

};

func("blah2");
甚至可以安照如下方式,使用Function構造器
var func = new Function("x", "alert(x);");

func("blah3");
上面展示了函式僅僅是一個支援函式呼叫操作的物件。最後一種方式是使用Function 構造器來定義一個函式,這種方式並不使用廣泛。但它很有趣,我們可以看出,函式的主體僅僅是Function 構造器的一個String 型別的引數。這意味著你可以在執行期間構造任意的函式。

為了進一步的演示 函式就是一個物件,我們可以為其它物件那麼為函式增加屬性。程式碼如下

function sayHi(x) {

    alert("Hi, " + x + "!");

}

sayHi.text = "Hello World!";

sayHi["text2"] = "Hello World... again.";
作為一個物件, 函式也能夠被分配給一個變數,或作為一個形參給傳遞其它函式,或作為其它函式的返回值,或作為一個物件的屬性,或作為一個數組中的元素,等等

程式碼片段一提供瞭如上的例子。

程式碼片段一  在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所示。

程式碼片段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方法使得該函式成為第一引數的中一個方法。即,無論哪一個物件作為第一個形參被傳進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 不同於C#,它沒有類的概念。在下面章節中,我們將會充分得利用函式作為構造器與new關鍵字在一起來實現例項化物件。

建構函式 但它不是類

對於javascript面向物件程式設計來說,有一件事很奇怪,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!

好吧,上面發生了什麼呢?我們看下面這行程式碼。
var spot = new DogConstructor("Spot");
“new”關鍵字所做的事情很簡單。首先它建立一個新的空物件。然後該函式緊跟著被執行,該新的物件設定為該函式中this對應的值。換句化說,上面程式碼跟下面的類似。
// create an empty object

var spot = {}; 

// call the function as a method of the empty object

DogConstructor.call(spot, "Spot");

我們從DogConstructor可以看出,呼叫該函式來初始化該物件,函式中的this關鍵字對應著呼叫期間的物件。這樣的話,我們可以有一種方式來為物件建立一個模版。

無論我什麼時候需要建立一個物件,我們只需要呼叫new關鍵字和建構函式,這樣我們得到一個初始化的物件。看起來與c++/C#的類很相似,是吧! 實際上,在javascript語言中,建構函式的名字就是類似於c++/C#中類的名字。在上面的例子中,我們為該建構函式取名為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");
在上面的例子中,我定義了一個名叫"name"的例項變數。這樣的話,每一個使用Dog作為建構函式的物件都擁有例項變數name 的副本,這正是我們所希望的。每一個物件都需要擁有屬性來儲存自己的狀態。但是當我們看下一行時,我們會發現每一個Dog的例項都擁有respondTo 方法的副本,這實在是一種浪費,我們只需要一個在所有物件例項中都能夠共享的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的所有例項僅僅共享respondTo方法的一個例項。但是這種寫法也有它的不足之處,假如方法很多的話,程式碼會變的難以維護。你的程式碼中會參雜著很多全域性變數,尤其是你擁有很多"類"的時候,如果它們又擁有類似的方法名稱,那會變得更糟糕。有一種更好的方法來解決該難題,那就是使用原型物件。下面的章節中,我們一起來討論該話題

原型(Prototypes)

原型物件在Javascript面向物件程式設計是一個很重要的內容。在JavaScript中有一個這樣的概念,每一個物件都是作為一個已經存在的物件(原型物件)的副本的而建立的。原型物件的所有屬性和方法都夠被該原型構造器所建立的物件擁有。當你按如下的方式建立一個新的Dog物件時,我們可以這樣說,該物件中屬性和方法都繼承於原型。

var buddy = new Dog("Buddy");
buddy物件從自己原型中繼承了屬性和方法,但我們無法從上面的一行程式碼中得知該原型從哪裡來。實際上,物件buddy的原型(prototyoe)來自建構函式的一個屬性(換句話說,就是函式Dog)。

在javascript中,每一個函式都有一個名為“prototyoe”的屬性,該屬性對應著一個原型物件。該原型物件也有一個名為“constructor”的屬性,"constructor"屬性指向函式自己本身。這是一個迴圈引用。圖一能夠更好的展現該關係。

圖一:每一個函式的原型都有一個constructor屬性。

當一個函式(比如上面的Dog)使用new 操作符來建立一個物件的時候,被建立的物件就繼承該函式原型的屬性。從圖一中,我們可以看到Dog.prototype物件有一個指向Dog函式的constructor屬性。因此每一個Dog物件(即繼承於Dog.prototype)都有一個指向Dog函式constructor屬性。下面的程式碼將會證實這一點。建構函式,原型物件,以及由他們建立的物件之間的關係將在圖二中展現。
 

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"));


圖2 例項繼承於原型

我們當中的一些人可能已經發現了這樣的一個現象,我們在上面的程式碼中呼叫了hasOwnProperty 和isPrototypeOf方法,但這些方法來自哪裡呢?它們並來源與Dog.prototype。事實上,像toString,toLoaclStrng ,valueOf這些方法,我們都可以通過Dog.prototype或者Dog的例項來呼叫,但是這些方法中任何一個都不來源於Dog.prototype。正如.Net Framework 有System.Object作為所有類的基類一樣,JavaScript中有Ojbect.prototype作為所有原型的的終極原型(Object.prototype的原型為null)。

在這個例子中,記住Dog.prototype 是一個物件。它通過呼叫Ojbect 建構函式來建立,但這是不可見的。

Dog.prototype = new Object();
就像Dog的例項繼承於Dog.prototype,Dog.prototype繼承於Object.prototype。這使得Dog的所有例項都繼承了Object.prototype的方法和屬性。
每一個JavaScript物件都繼承了一個原型鏈,所有的原型都終止於Object.prototype。目前我們看到的繼承都是活動物件之間的繼承,這不同於類之間的繼承(宣告的時候實現繼承),顯然,JavaScript中的繼承要靈活的多。我可以簡單的描述一下:當我們使用某一個物件的屬性/方法時,JavaScript將會檢查在這個物件中是否定義了該屬性/方法,如果沒有,該物件的原型將會被檢查是否定義了該屬性/方法,以此類推,一直檢測到Object.prototype。圖三描述這個解析過程。

圖三:解析toString()方法的原型鏈

JavaScript動態的解析屬性和方法呼叫的方式會導致一些後果:改變一個原型物件很容易在繼承於該原型的物件中顯示出來。如果我們在一個物件中定義了屬性/方法 X 。該物件原型中的相同名字屬性/方法將會被隱藏。例如,我們可以在Dog.prototype中定義一個toString方法來覆蓋Object.prototype中的toString方法。改變只是單向的,從原型物件到它派生的物件。

下面的程式碼向我們顯示了這種情況。下面的程式碼也解決了我們早先遇到的難題-----怎麼解決不必要的方法。我們可以把一個方法放入原型對像中,使得所有繼承該原型的物件能夠共享該方法。在下面的例子中,rover物件和spot物件共享同一個getBreed方法,直到在spot物件中重寫的getBreed方法(此處原文有錯處)。這樣sopt物件擁有了自己的getBreed()f方法,但是rover物件和繼承於GreatDane的物件仍然共享由GreateDane.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實際上是一門強大的,善於表達,簡潔的語言。它甚至擁有目前許多流行語言剛開始支援的特性。

JavaScript最大的特點之一是它支援閉包。在C#中通過匿名方法來實現閉包。閉包是一種執行現象:當一個內部函式(在C#中,內部匿名方法)與外部函式中的區域性變數繫結起來時會產生閉包。很顯然,這並沒有多大的意義,除非該內部函式和外部的某些變數關聯起來。舉一個例子會顯的更清晰點。

假設我們要過濾一個數字序列,就是大於100的數字能夠通過,其餘的被過濾掉,我們可以按如下方式來實現。

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());
};

從類繼承

到這裡,我們已經瞭解了建構函式和原型物件如何使您在 JavaScript 中模擬類。您已經看到,原型鏈可以確保所有物件都有 Object.prototype 的公用方法,以及如何使用閉包來模擬類的私有成員。但這裡還缺少點什麼。您尚未看到如何從類派生,這在 C# 中是每天必做的工作。遺憾的是,在 JavaScript 中從類繼承並非像在 C# 中鍵入冒號即可繼承那樣簡單,它需要進行更多操作。另一方面,JavaScript 非常靈活,可以有很多從類繼承的方式。 例如,有一個基類 Pet,它有一個派生類 Dog,如圖 四所示。這個在 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 呢?在圖 4 中可以看到,Dog 有另一個屬性 breed,它改寫了 Pet 的 toString 方法(注意,JavaScript 的約定是方法和屬性名稱使用 camel 大小寫,而不是在 C# 中建議的 Pascal 大小寫)。下面的程式碼 顯示如何這樣做。
// 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# 中,名稱空間用於儘可能地減少名稱衝突。例如,在 .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 學習經歷給予了我豐富的體驗,雖然其中也有一些挫折。如果本文能使您的體驗更加順利,我會非常高興,因為這正是我的目標。