做者:javascript
出處:html
最近,我面試了一位具備5年Web應用開發經驗的軟件開發人員。她有4年半的JavaScript編程經驗,自認爲本身具備很是優秀的JavaScript技能,但是,隨後我很快發現,實際上她對JavaScript卻知之甚少。然而,我並非要爲此而責怪她。JavaScript就是這麼難以想象。有不少人(也包括我本身,這種狀況直到最近纔有所改觀)都自覺得是,以爲由於他們懂C/C++/C#或者具備編程經驗,便覺得他們很是擅長JavaScript這門語言。java 從某個角度講,這種自覺得是也並不是毫無道理。用JavaScript作一些簡單的事情是很是容易的。其入門的門檻很是低;這個語言待人寬厚,並不苛求你必須懂它不少才能開始用它編寫代碼。甚至對於非程序員來講,也能夠僅花個把小時就可以上手用它爲他的網站編寫幾段或多或少都有些用的腳本。程序員 |
實際上直到最近,不管懂的JavaScript有多麼少,僅僅在MSDN® DHTML參考資料以及我在C++/C#方面編程經驗的幫助下,我都可以湊合過下去。直到我在工做中真正開始編寫AJAX應用時,我才發現我對JavaScript的瞭解有多麼欠缺。這種新一代的Web應用複雜的交互特性要求使用一種徹底不一樣的方式來編寫JavaScript代碼。這些都是很是嚴肅的JavaScript應用!咱們以往那種漫不經心編寫腳本的方法不靈了。面試 面向對象的編程(OOP)這種方法普遍用於多種JavaScript庫,採用這種方法可以使代碼庫更加易於管理和維護。JavaScript支持OOP,但它的支持方式同流行的Microsoft® .NET框架下的C++、C#、Visual Basic®等語言徹底不一樣,因此,大量使用這些語言的開發者起初可能會發現,JavaScript中的OOP比較怪異,同直覺不符。我寫這篇文章就是要對JavaScript究竟是如何支持面向對象編程的以及如何高效利用這種支進行面向對象的JavaScript開發進行深刻討論。接下來讓咱們開始談談對象(除了對象還能有別的嗎?)吧。算法 |
JavaScript對象是字典在C++或C#中,當談及對象時,咱們指的是類或者結構的實例。對象根據實例化出它的模版(也即,類)的不一樣而具備不一樣的屬性和方法。JavaScript對象不是這樣的。在JavaScript中,對象僅僅是name/value對的集合,咱們能夠把JavaScript對象看做字典,字典中的鍵爲字符串。咱們能夠用咱們熟悉的"." (點)操做符或者通常用於字典的"[]"操做符,來獲取或者設置對象的屬性。下面的代碼片斷編程 var userObject = new Object(); userObject.lastLoginTime = new Date(); alert(userObject.lastLoginTime);
同這段代碼所作的徹底是一樣的事情:api 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中字典則無此限制。閉包 這些例子也代表,同C++或者C#對象相比,JavaScript對象是多麼地更加具備可塑性。屬性lastLoginTime沒必要事先聲明,若是在使用這個屬性的時候userObject還不具備以此爲名的屬性,就會在userObject中把這個屬性添加進來。若是記住了JavaScript對象就是字典的話,你就不會對此大驚小怪了 —— 畢竟咱們隨時均可以把新鍵(及其對應的值)添加到字典中去。 JavaScript對象的屬性就是這個樣子的。那麼,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構造器的一個字符串型的參數。這就意味着,你能夠在JavaScript運行的時候構造出任意的函數。 |
要進一步證實函數是對象,你能夠就象爲任何其它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所示爲其中一例。 圖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的開發者也應該熟悉這種用法 —— 只不過"this"在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方法,它都會成爲這次函數調用中"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做爲構造器函數使用而建立的每一個對象都有本身的一份叫作name的實例變量(如前所述,name就是該對象的字典入口)。這符合咱們的指望;畢竟每一個對象都需屬於本身的一份實例變量,只有這樣才能保存它本身的狀態。可是若是你再看接下來的那行代碼,就會發現Dog的每一個實例都有本身的一份respondTo方法,這但是個浪費;respondTo的實例你只須要一個,只有將這一個實例在全部的Dog實例間共享便可!你能夠把respondTo的定義從Dog中拿出來,這樣就能夠克服此問題了,就向下面這樣:
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方法的同一個實例了。可是,隨着方法數量的增長,這種方式維護起來會愈來愈困難。最後你的代碼庫中會堆積大量的全局函數,並且,隨着「類」的數量不斷增長,特別是這些類的方法具備相似的方法名時,狀況會變得更加糟糕。這裏還有一個更好的辦法,就是使用原型對象,這就是下一個小節要討論的內容。 |
原型(Prototype)原型對象是JavaScript面向對象編程中的一個核心概念。原型這個名稱來自於這樣一個概念:在JavaScript中,全部對象都是經過對已有的樣本(也即,原型)對象進行拷貝而建立的。該原型對象的全部屬性和方法都會成爲經過使用該原型的構造函數生成的對象的屬性和方法。你能夠認爲,這些對象從它們的原型中繼承了相應的屬性和方法。當你象這樣來建立一個新的Dog對象時 var buddy = new Dog("Buddy");
buddy所引用的對象將從它的原型中繼承到相應的屬性和方法,雖然僅從上面這一行代碼可能會很難看出來其原型來自哪裏。buddy對象的原型來自來自構造器函數(在此例中指的就是函數Dog)的一個屬性。 |
在JavaScript中,每一個函數都有一個叫作「prototype」的屬性,該屬性指向一個原型對象。發過來,該原型對象據有一個叫作"constructor"的屬性,該屬性又指回了這個函數自己。這是一種循環引用;圖3 更好地揭示出了這種環形關係。
好了,當一個函數(好比上例中的Dog)和"new"操做符一塊兒使用,建立出一個對象時,該對象將從Dog.prototype中繼承全部的屬性。在圖3中,你能夠看出,Dog.prototype對象具備一個指會Dog函數的construtor屬性,每一個Dog對象(它們繼承自Dog.prototype)將一樣也具備一個指會Dog函數的constructor屬性。圖4中的代碼證實了這一點。構造器函數、原型對象以及用它們建立出來的對象這三者之間的關係如圖5所示。 |
圖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"));
有些讀者可能已經注意到了圖4中對hasOwnProperty方法和isPrototypeOf方法的調用。這些方法又來自哪裏呢?它們並非來自Dog.prototype。實際上,JavaScript中還有其它一些相似於toString、toLocaleString和valueOf等等咱們能夠直接對Dog.prototype以及Dog的實例進行調用的方法,但它們通通都不是來自於Dog.prototype的。其實就象.NET框架具備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所示即爲這個解析過程。
|
JavaScript這種動態解析屬性訪問和方法調用的方式將對JavaScript帶來一些影響。對原型對象的修改會立刻在繼承它的對象中得以體現,即便這種修改是在對象建立後才進行的也可有可無。若是你在對象中定義了一個叫作X的屬性/方法,那麼該對象原型中同名的屬性/方法就會沒法訪問到。例如,你能夠經過在Dog.prototype中定義一個toString方法來對Object.prototype中的toString方法進行重載。全部修改指揮在一個方向上產生做用,即慈寧宮原型到繼承它的對象這個方向,相反則否則。 圖7所示即爲這種影響。圖7還演示瞭如何解決前文碰到的避免沒必要要的方法實例問題。不用讓每一個對象都具備一個單獨的方法對象的實例,你能夠經過將方法放到其原型之中來讓全部對象共享同一個方法。此例中,getBreed方法由rover和spot共享 —— 至少直到在spot中重載了getBreed(譯者注:原文爲toString,應爲筆誤)方法以前。spot在重載以後就具備本身版本的getBreed方法,可是rover對象以及隨後使用new和GreatDane建立的對象仍將繼承的是定義於GreatDane.prototype對象的getBreed方法。 圖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#徹底相同。既然構造器函數就是類的名字,因此這也不該該有什麼奇怪的。這樣你就有了類、共有屬性/方法以及靜態屬性/方法。你還須要什麼呢?固然,還須要私有成員。可是,JavaScript並不直接支持私有成員(這方面它也不支持protected成員)。對象的因此屬性和方法全部人均可以訪問獲得。這裏有一種在類中定義出私有成員的方法,但要完成這個任務就須要首先對閉包有所瞭解。 |
閉包我學JavaScript徹底是無可奈何。由於我意識到,不學習JavaScript,就沒法爲在工做中參加編寫真正的AJAX應用作好準備。起初,我有種在程序員的級別中降低了很多等級的感受。(我要學JavaScript了!我那些使用C++的朋友該會怎麼說我啊?)可是一旦我克服了起初的抗拒心理以後,我很快發現,JavaScript其實是一門功能強大、表達能力極強並且很小巧的語言。它甚至擁有一些其它更加流行的語言纔剛剛開始支持的特性。 JavaScript中更加高級的一個特性即是它對閉包的支持,在C# 2.0中是經過匿名方法對閉包提供支持的。閉包是一種運行時的現象,它產生於內部函數(在C#中成爲內部匿名方法)本綁定到了其外部函數的局部變量之上的時候。顯然,除非內部函數能夠經過某種方式在外部函數以外也可讓其能夠訪問獲得,不然這也沒有多大意義。舉個例子就能夠把這個現象說得更清楚了。 |
假如你須要基於一個簡單評判標準對一個數字序列進行過濾,該標準就是大於100的數字能夠留下,但要把其它的因此數字都過濾掉。你能夠編寫寫一個如圖8所示的函數。 圖8 基於謂詞(Predicate)對元素進行過濾
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); var greaterThan300 = filter( function(x) { return (x > 300) ? true : false; }, someRandomNumbers);
|
可能還須要留下大於50、2五、十、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就離開了它的做用範圍!可是在此種狀況下,內部匿名函數仍然還攜帶着它,即便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就應該被認爲永遠消失了。然而,這兩個參數被4個內部函數捕獲,這些內部函數被賦值爲Person實例的方法了,所以這樣一來就使得name和age可以繼續存活下去,但卻被很嚴格地限制爲只有經過這4個方法才能訪問到它們。因此,你能夠這樣作: 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的特權方法方法來訪問私有成員: 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中的通用方法。你還看到了如何使用閉包來模擬出私有成員。可是,這裏好像仍是缺點什麼東西。你還沒看到在JavaScript中如何實現類的繼承;這在C#中但是司空見慣的事情。很不幸,在JavaScript進行類的繼承沒法象在C#中那樣鍵入一個冒號而實現;在JavaScript中還須要作更多的事情。但從另外一方面講,由於JavaScript很是靈活,咱們有多種途徑實現類的繼承。 比方說,如圖9所示,你有一個基類叫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類呢?從 圖9中可看出,Dog類具備一個額外的屬性,breed,,而且它還重載了Pet的toString方法(請注意,avaScript中的方法和屬性命名慣例採用的是駝峯式大小寫方式,即camel case;而C#推薦使用的是Pascal大小寫方式)。 圖10所示即爲Pet類的定義實現方法: 圖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測試在JavaScript中也可以正常進行。並且如你所願,特權方法也可以正常得以運行。 |
模擬命名空間在C++和C#中,命名空間用來將命名衝突的可能性減少到最小的程度。例如,在.NET框架中,命名空間能夠幫助咱們區分出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 and ASP.NET AJAX"的側邊欄。 |
你應該用這種方式來進行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規範(ECMA-262是JavaScript的官方規範)的第4個版本,JavaScript 2.0將具備真正的類。因此說,JavaScript正在逼近基於類的編程語言。然而,JavaScript 2.0要獲得普遍使用可能還須要幾年的時間。 同時還有一點也很重要,就是要全面掌握當前版本的JavaScript,只有這樣才能讀懂和編寫出基於原型和基於類的這兩種風格的JavaScript代碼。 |
大局觀隨着交互式、重客戶端AJAX應用的普及,JavaScript很快就成爲了.NET開發者工具箱中最有用的工具之一。然而,對於更加適應 C++、C#或者Visual Basic等語言的開發者來說,JavaScript的原型本性一開始會讓它們感到很不適應。我以爲個人JavaScript之旅收穫頗豐,但一直以來也不乏挫折打擊。若是這篇文章可以幫助你更加順利地進步,那麼我將倍感欣慰,由於這就是我寫這篇文章的目的所在。 OOP 和 ASP.NET AJAXASP.NET AJAX中實現的OOP同我在這篇文章裏討論的規範的實現方法稍有不一樣。這裏面主要有兩個方面的緣由:ASP.NET AJAX版的實現爲反射(對於象xml-scrip這樣的聲明式語法而且爲了參數驗證,反射是頗有必要的手段)提供了更多的可能,並且ASP.NET AJAX旨在將.NET開發者所熟悉的其它一些語法結構,好比屬性、事件、枚舉以及接口等翻譯爲JavaScript代碼。 在當前普遍可用的版本中,JavaScript缺少.NET開發者所熟知的大量OOP方面的概念,ASP.NET AJAX模擬出了其中的大部分概念。 類可用具備基於命名規範的屬性訪問器(下文中有例子),還可用徹底按照.NET所提供的模式進行事件多播。私有變量的命名聽從如下劃線打頭的成員就是私有成員這樣的規範。不多有必要使用真正私有的變量,這個策略使得咱們可用從調試器中直接查看這種變量。引入接口也是爲了進行類型檢查,而不是一般的duck-typing(一種類型方案,其基於的概念是,若是有一種東西象鴨子那樣走路而且象鴨子那樣嘎嘎叫,咱們就認爲這種東西是鴨子,或者說可用把這種東西看做鴨子)。 類和反射
在JavaScript中,咱們沒法得知函數的名字。即便有可能能夠得知,多數狀況下這對咱們來講也沒有什麼幫助,由於類構造器一般就是將一個匿名函數賦值爲一個命名空間變量。真正的類型名的是由該變量的全限定名組成的,但卻一樣沒法取得,構造器函數對此名也一無所知。爲了克服此侷限並在JavaScript類之中具備豐富的反射機制,ASP.NET AJAX要求要將類型的名字進行註冊。
ASP.NET AJAX中的反射API可用於任何類型,不管該類型是內建的類、接口、命名空間、甚至是枚舉都沒有問題,並且其中還包含有和.NET框架中相同的isInstanceOfType和inheritsFrom函數,這兩個函數用來在程序運行時對類的層次結構進行檢視。ASP.NET AJAX在調試模式還作了類型檢查,其意義在於可以幫助開發者儘早地找出程序中的bug。
註冊類的層次結構和基類的調用要在ASP.NET AJAX中定義一個類,你須要將該類的構造器函數賦值給一個變量(要注意構造器函數是如何調用基類的方法的): MyNamespace.MyClass = function() { MyNamespace.MyClass.initializeBase(this); this._myProperty = null; } 而後,你須要在它的原型中定義該類的成員: 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。
Ray Djajadinata來自新加坡的Barclays Capital公司,他正興致高昂地從事着AJAX應用的開發。你能夠經過這個Email同他聯繫:
ray.djajadinata@gmail.com.
|
英文原文:Create Advanced Web Applications With Object-Oriented Techniques
參與翻譯(1人):fbm