首頁 >web前端 >js教程 >悟透JavaScript

悟透JavaScript

高洛峰
高洛峰原創
2016-11-26 09:31:54965瀏覽

引子

    程式設計世界只存在兩種基本元素,一個是數據,一個是程式碼。程式設計世界就是在數據和程式碼千絲萬縷的糾纏中呈現出無限的生機和活力。

    資料天生就是文靜的,總想維持自己固有的本色;而代碼卻天生活潑,總想改變這個世界。
 
   你看,資料代碼之間的關係與物質能量間的關係有著驚人的相似。數據也是有慣性的,如果沒有程式碼來施加外力,她總是保持自己原來的狀態。而程式碼就像能量,他存在 的唯一目的,就是要努力改變資料原來的狀態。在程式碼改變資料的同時,也會因為資料的抗拒而反過來影響或改變程式碼原有的趨勢。甚至在某些情況下,數據可以轉 變成代碼,而代碼卻又有可能被轉變為數據,或許還存在一個類似E=MC2形式的數位轉換方程式呢。然而,就是在資料和程式碼間這種即矛盾又統一的運作中,總是能 體現出電腦世界的規律,這些規律正是我們所寫的程式邏輯。

    不過,由於不同程式設計師有著不同的世界觀,這些資料和程式碼看起來也不盡相同。於是,不同世界觀的程式設計師運用各自的方法論,推動著程式設計世界的進化與發展。
 
    眾所周知,當今最受歡迎的程式設計思想莫過於物件導向程式設計的想法。為什麼物件導向的思想能迅速風靡程式設計世界呢?因為物件導向的想法首次把資料和程式碼結合成統一 體,並以一個簡單的物件概念呈現給程式設計者。這一下子就將原來那些雜亂的演算法與子程序,以及糾纏不清的複雜資料結構,劃分成清晰而有序的物件結構,從而理清 了資料與程式碼在我們心中那團亂麻般的結。我們又可以有一個更清晰的思維,在另一個思想高度上去探索更浩瀚的程式設計世界了。

    在五祖弘忍講授完《對象真經》之後的一天,他對眾弟子們說:「經已講完,想必爾等應該有所感悟,請各自寫個偈子來看」。大弟子神秀是被大家公認為悟性最高 的師兄,他的偈子寫道:「身是對象樹,心如類般明。朝朝勤拂拭,莫讓惹塵埃!」。此偈一出,立刻引起師兄弟們的轟動,大家都說寫得太好了。只有火頭僧慧能 看後,輕輕地嘆了口氣,又隨手在牆上寫道:「對象本無根,類型亦無形。本來無一物,何處惹塵埃?」。然後搖了搖頭,揚長而去。大家看了慧能的偈子都說: 「寫的什麼亂七八糟的啊,看不懂」。師父弘忍看了神秀的詩偈也點頭稱讚,再看慧能的詩偈之後默然搖頭。就在當天夜裡,弘忍卻悄悄把慧能叫到自己的禪房,將珍​​藏多年的軟體真經傳授於他,然後讓他趁著月色連夜逃走...

    後來,慧能果然不負師父厚望,在南方開創了禪宗另一個廣闊的天空。而慧能當年帶走的軟體真經中就有一本是《JavaScript真經》!

迴歸簡單

    要理解JavaScript,你得先放下物件和類別的概念,回到資料和程式碼的本原。前面說過,程式設計世界只有資料和程式碼兩種基本元素,而這兩種元素又有著糾纏不清的關係。 JavaScript就是把資料和程式碼都簡化到最原始的程度。

    JavaScript中的資料很簡潔的。簡單資料只有 undefined, null, boolean, number和string這五種,而複雜資料只有一種,即object。這就好比中國古典的樸素唯物思想,把世界最基本的元素歸為金木水火土,其他複雜 的物質都是由這五種基本元素組成。

    JavaScript中的程式碼只體現為一種形式,就是function。

    注意:以上單字都是小寫的,不要和Number, String, Object, Function等JavaScript內建函數混淆了。要知道,JavaScript語言是區分大小寫的呀!

    任何一個JavaScript的標識、常數、變數和參數都只是unfined, null, bool, number, string, object 和function類型中的一種,也就typeof值所顯示的類型。除此之外沒有其他類型了。

    先說說簡單資料型別吧。

    undefined:   代表一切未知的事物,啥都沒有,無法想像,程式碼也就更無法去處理了。
                      注意:typeof(undefined) 回收也是 undefined。
                           地

    null:            有那麼一個概念,而沒有東西。無中似有,有中還無。雖難以想像,但已經可以用程式碼來處理了。
                      注意:typeof(null)回傳object,但null並非object,且具有null值的變數也並非object。

    boolean:      是就是,非就非,沒有疑慮。對就對,錯就錯,絕對明確。既能被程式碼處理,也可以控製程式碼的流程。

    number:      線性的事物,大小且次序分明,多而不亂。便於程式碼進行批次處理,也控製程式碼的迭代和循環等。
                      注意:typeof(NaN)及typeof(Infinity)都回至number 。
                              NaN參與任何數值且計算的結構為NaN,且 NaN != NaN 。
                              以 Infinity/ Infinity = NaN 。

    string:         人類導向的理性事物,而非機器訊號。人機訊息溝通,代碼據此理解人的意圖等等,都靠它了。

     簡單型別都不是對象,JavaScript並沒有將對象化的能力賦予這些簡單型別。直接被賦予簡單型別常數值的識別符、變數和參數都不是一個物件。

    所謂“物件化”,就是可以將資料和程式碼組織成複雜結構的能力。 JavaScript中只有object型別和function型別提供了物件化的能力。

沒有類別

    object就是物件的型別。在JavaScript中不管多麼複雜的資料和程式碼,都可以組織成object形式的物件。

    但JavaScript卻沒有 「類別」的概念!

    對許多物件導向的程式設計師來說,這恐怕是JavaScript中最難以理解的地方。是啊,幾乎任何講物件導向的書中,第一個要講的就是「類」的概 念,可是物件導向的支柱。這突然沒有了“類”,我們就像一下子沒了精神支柱,感到六神無主。看來,要放下對象和類,達到「對象本無根,類型亦無形」的境 界確實是件不容易的事情啊。

    這樣,我們先來看看JavaScript程式:
    var life = {};
    for(life.age = 1; life.age case 1: life.body = "卵細胞";
                    life.say = function(){alert break;
            case 2: life.tail = "尾巴";
                           gill = "腮";
                    life.body = "蝌蚪";
      "-"+this.tail+","+this.gill)};
                      delete life.gill;
                    life.legs = "四條腿                       life.body = "青蛙";
            = function(){alert(this.age+this.body+"-"+this.legs+","+this.lung)};
                .say();
    };

    這段JavaScript程式一開始產生了一個生命對象life,life誕生時只是一個光溜溜的對象,沒有任何屬性和方法。在第一次生命過程中,它有了 一個身體屬性body,並有了一個say方法,看起來是一個「卵細胞」。在第二次生命過程中,它又長出了“尾巴”和“腮”,有了tail和gill屬性, 顯然它是一個“蝌蚪”。在第三次生命過程中,它的tail和gill屬性消失了,但又長出了“四條腿”和“肺”,有了legs和lung屬性,從而最終變 成了“青蛙”。如果,你的想像力豐富的話,或許還能讓它變成英俊的“王子”,娶個美麗的“公主”什麼的。不過,看完這段程式之後,請你思考一個問題:

    我們一定需要類嗎?

    還記得兒時那個「小蝌蚪找媽媽」的童話嗎?也許就在昨天晚,你的孩子剛好是在這個美麗的童話中進入夢鄉的吧。可愛的小蝌蚪也就是在其自身類型不斷演化過程 中,逐漸變成了和媽媽一樣的“類”,從而找到了自己的媽媽。這個童話故事中蘊含的程式設計哲理是:物件的「類」是從無到有,又不斷演化,最終又消失於無形之中的...

    “類”,的確可以幫助我們理解複雜的現實世界,這紛亂的現實世界也的確需要分類。但如果我們的思想被「類」束縛住了,「類」也就變成了「累」。想像一 下,如果一個生命對象開始的時就被規定了固定的“類”,那麼它還能演化嗎?蝌蚪還能變成青蛙嗎?還可以跟孩子們講小蝌蚪找媽媽的故事嗎?

    所以,JavaScript中沒有“類別”,類別已化於無形,與物件融為一體。正是由於放下了「類別」這個概念,JavaScript的物件才有了其他程式語言所沒有的活力。

    如果,此時你的內心深處開始有所感悟,那麼你已經逐漸開始理解JavaScript的禪機了。

函數的魔力

    接下來,我們再來討論JavaScript函數的魔力。

    JavaScript的程式碼只有function一種形式,function就是函數的型別。也許其他程式語言還有procedure或 method等程式碼概念,但在JavaScript裡只有function一種形式。當我們寫下一個函數的時候,只不過是建立了一個function類型 的實體而已。請看下面的程式:
    function myfunc()
    {
        alert("hello");
    運行之後可以看到typeof(myfunc)回傳的是function。以上的函數寫法我們稱為「定義式」的,如果我們將其改寫成下面的「變項式」的,就更容易理解了:
    var myfunc = function ()
       ;

        };

   
    alert(typeof(myfunc));

    這裡明確定義了一個變數myfunc,而它的初始值被賦予了一個function的實體。因此,typeof(myfunc)回傳的也是function。 其實,這兩種函數的寫法是等價的,除了一點細微差別,其內部實作完全相同。也就是說,我們寫的這些JavaScript函數只是一個命了名的變數而已,其 變數型別即為function,變數的值就是我們寫的函數程式碼體。

    聰明的你或許立即會進一步的追問:既然函數只是變量,那麼變量就可以被隨意賦值並用到任意地方???/p>

    讓我們來看看下面的代碼: )

        {

            alert("hello");

         myfunc = function ()

        {

            alert("yeah");
  myfunc(); //第二次呼叫myfunc,將輸出yeah

    這個程式運作的結果告訴我們:答案是肯定的!在第一次呼叫函數之後,函數變數又被賦予了新的函數程式碼體,使得第二次呼叫函數時,出現了不同的輸出。

    好了,我們又來把上面的程式碼改成第一個定義式的函數形式:
    function myfunc ()
    {
   呼叫myfunc,輸出yeah而不是hello
   
    function myfunc ()
    {

        alert("yeah"); yeah

    照理說,兩個簽名完全相同的函數,在其他程式語言中應該是非法的。但在JavaScript中,這沒錯。不過,程式運行之後卻發現一個奇怪的現象:兩次呼叫都只是最後那個函數裡輸出的值!顯然第一個函數沒有起到任何作用。這又是為什麼呢?


    原來,JavaScript執行引擎並非一行一行地分析和執行程序,而是一段一段地分析執行的。而且,在同一段程式的分析執行中,定義式的函數語句會被提 取出來優先執行。函數定義執行完之後,才會依序執行其他語句程式碼。也就是說,在第一次呼叫myfunc之前,第一個函數語句定義的程式碼邏輯,已經被第二個 函數定義語句覆寫了。所以,兩次都呼叫都是執行最後一個函數邏輯了。

    如果把這個JavaScript程式碼分成兩段,例如將它們寫在一個html中,並用<script></script>標籤分成這樣的兩塊:
<script><br/>    function myfunc ( ");<br/>    };<br/>    myfunc(); //這裡叫myfunc,輸出hello<br/></script>

<script><br/>    function    };   </script>

    myfunc( ); //這裡呼叫myfunc,輸出yeah

    這時,輸出才是各自依序來的,也證明了JavaScript的確是一段段地執行的。

    一段程式碼中的定義式函數語句會優先執行,這似乎有點像靜態語言的編譯概念。所以,這個特徵也被有些人稱為:JavaScript的「預編譯」。

    大多數情況下,我們也沒有必要去糾纏這些細節問題。只要你記住一點:JavaScript裡的程式碼也是一種數據,同樣可以被任意賦值和修改的,而它的值就是程式碼的邏輯。只是,與一般資料不同的是,函數是可以被呼叫執行的。

    不過,如果JavaScript函數只有這點道行的話,這與C++的函數指針,DELPHI的方法指針,C#的委託相比,又有啥稀奇嘛!然 而,JavaScript函數的神奇之處也反映在另外兩個面向:一是函數function類型本身也具有物件化的能力,二是函數function與物件 object超然的結合能力。

奇妙的物件

    先來說說函數的物件化能力。

    任何一個函數都可以為其動態地添加或去除屬性,這些屬性可以是簡單類型,可以是對象,也可以是其他函數。也就是說,函數具有物件的全部特徵,你完全可以把 函數當作物件來用。其實,函數就是對象,只不過比一般的對像多了一個括號「()」運算符,這個運算子用來執行函數的邏輯。即,函數本身還可以被調用,一般對 象卻不可以被調用,除此之外完全相同。請看下面的程式碼:
    function Sing()
    {
        with(arguments.callee)
     hor = "李白";
    Sing.poem = "漢家秦地月,流影照明妃。月落陰山前。將author和poem屬性設為不同的作者和詩句,在 呼叫Sing()時就能顯示出不同的結果。這個範例用一種詩情畫意的方式,讓我們了解JavaScript函數就是物件的本質,也感受到了 JavaScript語言的優美。

    好了,以上的敘述,我們應該算理解了function類型的東西都是和object類型一樣的東西,這種東西被我們稱為「物件」。我們的確可以這樣去看待這些“物件”,因為它們既有“屬性”也有“方法”嘛。但下面的程式碼會讓我們產生新的疑惑:
    var anObject = {};  //一個物件
    anObject.aProperty = "Property of object";  //物件的一個屬性
  alert("Method of object")}; //物件的一個方法
    //主要看下面:

    alert(anObject["aProperty"]);   //可以將物件當陣列以屬性名稱作為下標來存取屬性

    anObject["aMethod"]();          //使用物件當數組以方法名稱為下標來呼叫方法

       alert( s + " is a " + typeof(anObject[s]));


    同樣地對function類型的物件相同:
    var aFunction = function() {};  //一個函數
  var aFunction = function() {};  //一個函數
 ";  //函數的一個屬性
    aFunction.aMethod = function(){alert("Method of function")}; //函數的一個方法
    //主看下面:
]    alert(aFunction["aProperty")    alert(aFunction["aProperty") ;   //可以將函數當數組以屬性名稱作為下標來存取屬性
    aFunction["aMethod"]();          //可以將函數當數組以方法名稱為下標來呼叫方法名)           //遍歷函數的所有屬性和方法進行迭代化處理

        alert(s + " is a " + typeof(aFunction[s]));

    是的,物件和函數可以像數組一樣,用屬性名或方法名作為下標來存取並處理。那麼,它到底該算是數組呢,還是算對象?

    我們知道,陣列應該算是線性資料結構,線性資料結構一般有一定的規律,適合進行統一的批次迭代操作等,有點像波。而物件是離散資料結構,適合用來描述分散的和個人化的東西,有點像粒子。因此,我們也可以這樣問:JavaScript裡的物件到底是波還是粒子?

    如果存在對象量子論,那麼答案一定是:波粒二象性!

    因此,JavaScript裡的函數和物件既有物件的特徵也有陣列的特徵。這裡的陣列稱為“字典”,一種可以任意伸縮的名稱值對兒的集合。其實, object和function的內部實作就是字典結構,但這種字典結構卻透過嚴謹而精巧的語法展現了豐富的外觀。正如量子力學在某些地方用粒子來 解釋和處理問題,而在另一些地方卻用波來解釋和處理問題。你也可以在需要的時候,自由選擇用物件還是陣列來解釋和處理問題。只要善於掌握 JavaScript的這些奇妙特性,就可以寫出許多簡潔而強大的程式碼來。

放下對象

    我們再來看看function與object的超然結合吧。

    在物件導向的程式設計世界裡,資料與程式碼的有機結合就構成了物件的概念。自從有了對象,程式設計世界就被分割成兩個部分,一個是對象內的世界,一個是對像外的世界。 對像天生具有自私的一面,外面的世界未經允許是不可訪問的對象內部。物件也有大方的一面,它對外提供屬性和方法,也為他人服務。不過,在這裡我們要談到一 個有趣的問題,就是「對象的自我意識」。

    什麼?沒聽錯吧?對像有自我意識?

    可能對許多程式設計師來說,這的確是第一次聽到。不過,請君去看C++、C#和Java的this,DELPHI的self,還有VB的me,或許你會恍然大悟!當然,也可能只是說句「不過如此」而已。

    然而,就在物件將世界分割為內外兩部分的同時,物件的「自我」也隨之產生。 「自我意識」是生命最基本的特徵!正是由於物件這種強大的生命力,才使得程式設計世界充滿無限的活力與活力。

    但對象的「自我意識」在帶給我們快樂的同時也帶來了痛苦和煩惱。我們給了對象太多慾望,總希望它們能做更多的事。然而,對象的自私使得它們互相爭搶 系統資源,對象的自負讓對象變得複雜和臃腫,對象的自欺也往往帶來揮之不去的錯誤和異常。我們為什麼會有這麼多的痛苦和煩惱呢?
 
    為此,有一個人,在對象樹下,整整想了九九八十一天,終於悟出了生命的痛苦來自於慾望,但究其慾望的根源是來自於自我意識。於是他放下了“自我”,在對象 樹下成了佛,從此他開始普度眾生,傳播真經。他的名字就叫釋迦摩尼,而《JavaScript真經》正是他所傳經書中的一本。

    JavaScript中也有this,但這個this卻與C++、C#或Java等語言的this不同。一般程式語言的this就是物件自己,而 JavaScript的this卻不一定! this可能是我,也可能是你,可能是他,反正是我中有你,你中有我,這就不能用原來的那個「自我」來理解 JavaScript這個this的意思了。為此,我們必須先放下原來對象的那個「自我」。

    我們來看下面的程式碼:
    function WhoAmI()       //定義一個函數WhoAmI
    {   };
   
WhoAmI();   //此時是this目前這段程式碼的全域對象,在瀏覽器中就是window對象,其name屬性為空字串。輸出:I'm of object

    var BillGates = {name: "Bill Gates"};
    BillGates.WhoAmI = WhoAmI;  //將函數WhoAmI作為BillGates的方法。
    BillGates.WhoAmI();         //此時的this是BillGates。輸出:I'm Bill Gates of object
   
    var SteveJobs = {name: "Steve Jobs"};
    SteveJobs.WhoAmI = WhoAmI;  //將函數WhoAmI作為SteveJobs的方法。
    SteveJobs.WhoAmI();         //此時的this是SteveJobs。輸出:I'm Steve Jobs of object

    WhoAmI.call(BillGates);     //直接將BillGates作為this,呼叫WhoAmI。輸出:I'm Bill Gates of object
    WhoAmI.call(SteveJobs);     //直接將SteveJobs作為this,呼叫WhoAmI。輸出:I'm Steve Jobs of object
   
    BillGates.WhoAmI.call(SteveJobs);   //將SteveJobs當作this,卻呼叫BillGates的WhoAmII方法。輸出:I'm Steve Jobs of object
    SteveJobs.WhoAmI.call(BillGates);   //將BillGates作為this,卻呼叫SteveJobs的WhoAmI方法。輸出:I'm Bill Gates of object

    WhoAmI.WhoAmI = WhoAmI;     //將WhoAmI函數設定為自身的方法。
    WhoAmI.name = "WhoAmI";
    WhoAmI.WhoAmI();            //此時的this是WhoAmI函數自己。輸出:I'm WhoAmI of function
       
    ({name: "nobody", WhoAmI: WhoAmI}).WhoAmI();    //暫時建立一個匿名物件並設定屬性後呼叫WhoAmI方法。輸出:I'm nobody of object

    從上面的程式碼可以看出,同一個函數可以從不同的角度來調用,this並不一定是函數本身所屬的物件。 this只是在任意物件和function元素結合時的概念,是種結合比起一般物件語言的預設結合更靈活,顯得更超然和灑脫。

    在JavaScript函數中,你只能把this看成目前要服務的「這個」物件。 this是一個特殊的內建參數,根據this參數,您可以存取到「這個」 物件的屬性和方法,但卻不能給this參數賦值。在一般物件語言中,方法體程式碼中的this可以省略的,成員預設都先是「自己」的。但 JavaScript卻不同,由於不存在“自我”,當訪問“這個”物件時,this不可省略!

    JavaScript提供了傳遞this參數的多種形式和手段,其中,象BillGates.WhoAmI()和SteveJobs.WhoAmI()這種形式,是傳遞this參數最正規的形式,此時的this就是函數所屬的對象本身。而大多數情況下,我們幾乎很少去採用那些借花仙佛的呼喚形式。但只我 們要明白JavaScript的這個“自我”與其他程式語言的“自我”是不同的,這是放下了的“自我”,這就是JavaScript特有的世界觀。

對像素描

    已經說了許多了許多話題了,但有一個很基本的問題我們忘了討論,那就是:怎樣建立對象?

    在前面的範例中,我們已經涉及了物件的建立了。我們使用了一種被稱為JavaScript Object Notation(縮寫JSON)的形式,翻譯為中文就是「JavaScript物件表示法」。

    JSON為創建物件提供了一個非常簡單的方法。例如,
    建立一個沒有任何屬性的物件:
var o = {};

    建立物件並設定屬性及初始值:
var person = {name: "Angel", age: 18, married: false};

    建立一個物件並設定屬性和方法:
var speaker = {text: "Hello World", say: function(){alert(this.text)}};

     創造一個更複雜的對象,嵌套一個更複雜的對象,嵌套其他物件與物件陣列等:
    var company =
    {
        name: "Microsoft",
      53, Married: true},
        employees: [ {name: "Angel", age: 26, Married: false}, {name: "Hanson", age: 32, Marred: true}],
        readme: function() {document.write(thisctname + "product " + this.product);}
    };

    JSON的形式就是用大括「{}」號包含起來的項目列表,每一個項目間並用逗號「,」分隔,而項目就是用冒號「:」分隔的屬性名稱和屬性值。這是典型的字典 表示形式,也再次顯示了 JavaScript裡的物件就是字典結構。不管多麼複雜的對象,都可以被一句JSON程式碼來建立並賦值。

    其實,JSON就是JavaScript物件最好的序列化形式,它比XML更簡潔也更省空間。物件可以作為一個JSON形式的字串,在網路間自 由傳遞和交換訊息。而當需要將這個JSON字串變成一個JavaScript物件時,只需要使用eval函數這個強大的數字轉換引擎,就立即能得到一個 JavaScript記憶體物件。正是由於JSON的這種簡單樸素的天生麗質,才使得她在AJAX舞台上成為璀璨奪目的明星。

    JavaScript就是這樣,把物件導向那些看似複雜的東西,用及其簡潔的形式表達出來。卸下對象浮華的濃妝,還對像一個眉目清晰!

構造物件
 
    好了,接下我們來討論一下物件的另一種建立方法。

    除JSON外,在JavaScript中我們可以使用new運算元結合一個函數的形式來建立物件。例如:
    function MyFunc() {};         //定義一個空函數
    var anObj = new MyFunc(); 方式可真有意思,如何去理解這種寫法呢?

 

   其實,可以將上面的程式碼改寫成這個等價形式:
    function MyFunc(){};
    var anObj = {} all作為this指針呼叫MyFunc函數

    我們就可以這樣理解,JavaScript先用new操作符創建了一個對象,緊接著就將這個對像作為this參數調用了後面的函數。其實,JavaScript內部就是這麼做的,而且任何函數都可以這樣呼叫!但從「anObj = new MyFunc()」 這種形式,我們又看到一個熟悉的身影,C++和C#不就是這樣創建物件的嗎?原來,條條大路通靈山,殊途同歸啊!

    君看到此處或許會想,我們為什麼不可以把這個MyFunc當作構造函數呢?恭喜你,答對了! JavaScript也是這麼想的!請看下面的程式碼:

 1     function Person(name)   //帶參數的建構子

 2     {

 3         this.name = name;name;  this.SayHello = function () {alert("Hello, I'm " + this.name);};   //為this物件定義一個SayHello方法。
 5     };
 6
 7     function Employee(name, salary)     //子建構子
 8       //將this傳給父建構子
10         this.salary = salary ;       //設定一個this的salary屬性
11         this.ShowMeTheMoney = function() {alert(this.name + " $" + this.salary);};
12     };
13    Sharp
14    polyee構造函數建立SteveJobs物件
16
17     BillGates.SayHello();   //顯示:I'm Bill Gates
18     SteveJobs.SayHello //顯示:Ihowm Jobs)顯示:Ihowm Jobs)顯示:Ihow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehow>Thehowk/To);顯示:Steve Jobs $1234
20
21     alert(BillGates.constructor == Person);  //顯示:true
22     alert(SteveJobs.constructor == Employ); s.SayHello == SteveJobs.SayHello); //顯示:false

    這段程式碼表明,函數不但可以當作建構函數,還可以帶參數,還可以為物件添加成員和方法。其中的第9行,Employee建構子將自己接收的 this當作參數呼叫Person建構函數,這就是相當於呼叫基底類別的建構子。第21、22行也顯示這樣一個意思:BillGates是由Person構 造的,而SteveJobs是由Employee建構的。物件內建的constructor屬性也指明了建構物件所用的具體函數!

    其實,如果你願意把函數當作“類”的話,她就是“類”,因為她本來就有“類”的那些特徵。難道不是嗎?她生出的兒子各個都有相同的特徵,而且構造函數也與類別同名嘛!

    但要注意的是,以建構子操作this物件創建出來的每一個對象,不但有各自的成員數據,而且還有各自的方法數據。換句話說,方法的程式碼體(體現函數 邏輯的資料)在每一個物件中都存在一個副本。儘管每一個程式碼副本的邏輯是相同的,但物件確實是各自保存了一份代碼體。上例中的最後一句說明了這一實事, 這也解釋了JavaScript中的函數就是物件的概念。

    同一類別的物件各自有一個方法代碼顯然是一種浪費。在傳統的物件語言中,方法函數並不像JavaScript那樣是個物件概念。即使也有像函數指標、方法指標或委託那樣的變化形式,但其實質也是對同一份代碼的引用。一般的對象語言很難遇到這種情況。

    不過,JavaScript語言有極大的彈性。我們可以先定義一份唯一的方法函數體,並在建構this物件時使用這唯一的函數物件作為其方法,就能共享方法邏輯。例如:
    function SayHello()     //先定義一份SayHello函數碼
    {
        alert("Hello, I'm " + this.name); name)   //帶參數的建構函數
    {
        this.name = name;   //將參數值賦值給this物件的屬性www.2cto.com
       this this.2cto.com
       this.SayHello this.2cto.com
      ello this.SayHello =Haym/Saym/LaySm/LaySm/LayM.SayHello +SaySm/Lay
    };

    var BillGates = new Person("Bill Gates");   //建立BillGates物件

    var SteveJobs = new Person("Steve Jobs");    var SteveJobs = new Person("Steve Jobs");? ello == SteveJobs .SayHello); //顯示:true

    其中,最後一行的輸出結果顯示兩個物件確實共用了一個函數物件。雖然,這段程式達到了共享了一份方法程式碼的目的,但卻不怎麼優雅。因為,定義 SayHello方法時反映不出其與Person類別的關係。 「優雅」這個詞用來形容程式碼,也不知道是誰先提出來的。不過,這個詞反映了程式設計師已經從追求代 碼的正確、高效、可靠和易讀等基礎上,向著追求代碼的美觀感覺和藝術境界的層次發展,程序人生又多了些浪漫色彩。

   顯然,JavaScript早想到了這個問題,她的設計者們為此提供了一個有趣的prototype概念。

初看原型

    prototype源自法語,軟體界的標準翻譯為“原型”,代表事物的初始形態,也含有模型和樣板的意義。 JavaScript中的prototype概念恰如其分地反映了這個詞的內含,我們不能將其理解為C++的prototype那種預先聲明的概念。

    JavaScript的所有function類型的物件都有prototype屬性。這個prototype屬性本身又是一個object類型的對 象,因此我們也可以為這個prototype物件添加任意的屬性和方法。既然prototype是物件的“原型”,那麼由該函數建構出來的物件應該都會具 有這個“原型”的特性。事實上,在建構子的prototype上定義的所有屬性和方法,都是可以透過其建構的物件直接存取和呼叫的。也可以這麼 說,prototype提供了一群同類物件共享屬性和方法的機制。

    我們先來看看下面的程式碼:
    function Person(name)
    {
         .prototype.SayHello = function()  //為Person函數的prototype加入SayHello方法。
    {
        alert("Hello, I'm " + this.name);
    }

   遠Steve Jobs" );   //建立SteveJobs物件

    BillGates.SayHello();   //透過BillGates物件直接呼叫到SayHello方法

    SteveJobs.SayHello();LBillGatemBilldate通過
    SteveJobs.SayHello();LBillGate.FayBillTirFalkBillal.Falk 好的= SteveJobs.SayHello); //因為兩個物件是共享prototype的SayHello,所以顯示:true

    程式運行的結果表明,構造函數的prototype上定義的方法確實可以透過物件直接呼叫到,而且程式碼是共享的。顯然,把方法設定到prototype的 寫法顯得優雅多了,儘管呼叫形式沒有變,但邏輯上卻體現了方法與類別的關係,相對前面的寫法,更容易理解和組織程式碼。

    那麼,對於多層次類型的建構函數情況又如何呢?

    再來看下面的程式碼:

 1     function Person(name)   //基類建構子

 2     { 5    

 6     Person.prototype.SayHello = function() //為基類建構函數的prototype新增方法

 7     {

 8         alert("Hello, I'm " + this.name);
 salary) //子類別建構子
12     {
13         Person.call(this, name);    //呼叫基底建構子
14      
17     Employee.prototype = new Person();  //建造一個基底類別的物件作為子類別原型的原型,這裡很有意思
18    
19     Employee.prototype.ShowMeTheMoney = function()  // name + " $" + this.salary);
22     };
23
24     var BillGates = new Person("Bill Gates");  費用Jobs", 1234);   //建立子類別Employee的SteveJobs物件
26
27     BillGates.SayHello();       //與對象直接呼叫基底類別prototype的方法,關注!
29     SteveJobs.ShowMeTheMoney(); //透過子類別物件直接呼叫子類別prototype的方法
30
31     alert(BillGates.SayHello == SteveJobs.SayHello);

    這段程式碼的第17行,建構了一個基底類別的對象,並將其設為子類別建構函數的prototype,這是很有趣的。這樣做的目的就是為了第28行,透過子類別物件也可以直接呼叫基底類別prototype的方法。為什麼可以這樣呢?

    原來,在JavaScript中,prototype不但能讓對象共享自己財富,而且prototype還有尋根問祖的天性,從而使得先輩們的遺產可以代 代相傳。當從一個物件讀取屬性或呼叫方法時,如果該物件本身不存在這樣的屬性或方法,就會去自己關聯的prototype物件那裡尋找;如果prototype沒有,又會去prototype自己關聯的前輩prototype那裡尋找,直到找到或追溯過程結束。

    在JavaScript內部,物件的屬性和方法追溯機制是透過所謂的prototype鏈來實現的。當用new運算元建構物件時,也會同時將建構函式的 prototype物件指派給新建立的對象,成為該物件內建的原型對象。物件內建的原型物件應該是對外不可見的,儘管有些瀏覽器(如Firefox)可以 讓我們存取這個內建原型對象,但並不建議這樣做。內建的原型對象本身也是對象,也有自己關聯的原型對象,這樣就形成了所謂的原型鏈。

    在原型鏈的最末端,就是Object建構函式prototype屬性所指向的那一個原型物件。這個原型物件是所有物件最老的祖先,這個老祖宗實作了諸如 toString等所有物件天生就該具有的方法。其他內建建構函數,如Function, Boolean, String, Date和RegExp等的prototype都是從這個老祖宗傳承下來的,但他們各自又定義了自身的屬性和方法,從而他們的子孫就表現出各自宗族的那些特徵。

    這不就是「繼承」嗎?是的,這就是“繼承”,是JavaScript特有的“原型繼承”。

    「原型繼承」是慈祥而又嚴厲的。原形物件將自己的屬性和方法無私地貢獻給孩子們使用,也不強迫孩子們必須遵從,允許一些頑皮孩子按自己的興趣和愛好獨立 行事。從這點上看,原型對像是一位慈祥的母親。然而,任何一個孩子雖然可以我行我素,但卻不能動原型對象既有的財產,因為那可能會影響到其他孩子的利益。 從這一點上看,原型對象又像一位嚴厲的父親。讓我們來看看下面的程式碼就可以理解這個意思了:
    function Person(name)
    {
        this.name = name;
  
    Person.prototype.SayHello = function()  //原型的方法
    {
        alert("Hello, I'm " + this.name + "f " + this.comp alert("Hello, I'm " + this.name + "f " + this.comp alert("Hello, I'm " + this.name + "f " + this.comp). var BillGates = new Person("Bill Gates");
    BillGates.SayHello();   //由於繼承了原型的東西,規則矩矩輸出:Hello, I'm Bill Gates
   
    var SteveJobs = new Person("Steve  
    var SteveJobs = new Person("Steve  );
    SteveJobs.company = "Apple";    //設定自己的company屬性,掩蓋了原型的company屬性
    SteveJobs.SayHello = function() //實現了自己的SayHello方法,掩蓋了原型的SayHello = function() // ("Hi, " + this.name + " like " + this.company + ", ha ha ha ");
    };

    SteveJobs.SayHello();   //都是自己涵蓋的屬性和方法,輸出: Hi, Steve Jobs like Apple, ha ha ha
   
    BillGates.SayHello();   //SteveJobs的覆蓋沒有影響原型對象,BillGates還是按老樣子輸出
原型物件也可以掩蓋上層建構函數原型物件既有的屬性和方法。這種掩飾其實只是在物件自己身上創造了新 的屬性和方法,只不過這些屬性和方法與原型物件的那些同名而已。 JavaScript就是用這簡單的掩蓋機制實現了物件的「多型」性,與靜態物件語言的虛 函數和重載(override)概念不謀而合。

    然而,比靜態物件語言更神奇的是,我們可以隨時為原型物件動態添加新的屬性和方法,從而動態地擴展基類的功能特性。這在靜態物件語言中是很難想的。我們來看看下面的程式碼:
    function Person(name)
    {
        this.name = name;
       {
        alert(" Hello, I'm " + this.name);
    };
   
    var BillGates = new Person("Bill Gates");   //建立物件 Person.prototype.Retire = function ()    //建立物件後動態擴充原型的方法
    {
        alert("Poor " + this.name + ", bye bye!"); 方法即可被先前建立的對象立即調用

    阿彌佗佛,原型繼承竟然可以玩出有這樣的法術!

原型擴充

    想必君的悟性極高,可能你會這樣想:如果在JavaScript內建的那些如Object和Function等函數的prototype上添加些新的方法和屬性,是不是就能擴展JavaScript的功能呢?

    那麼,恭喜你,你得到了!

    在AJAX技術快速發展的今天,許多成功的AJAX專案的JavaScript運作庫都大量擴展了內建函數的prototype功能。例如微軟的 ASP.NET AJAX,就為這些內建函數及其prototype增加了大量的新特性,從而增強了JavaScript的功能。

    我們來看一段是摘自MicrosoftAjax.debug.js中的程式碼:

String.prototype.trim = function String$trim() {

    if (arguments.length !== 0)

    if (arguments.length !== 0) 2753. this.replace(/^s+|s+$/g, '');

}

    這段程式碼就是為內建String函數的prototype擴充了一個trim方法,於是所有的String類別物件都有了trim方法了。有了這個 擴展,今後要去除字串兩段的空白,就不用再分別處理了,因為任何字串都有了這個擴展功能,只要調用即可,真的很方便。

    當然,幾乎很少有人去為Object的prototype添加方法,因為那會影響到所有的對象,除非在你的架構中這種方法的確是所有對像都需要的。

    前兩年,微軟在設計AJAX類別庫的初期,用了一種被稱為「閉包」(closure)的技術來模擬「類別」。其大致模型如下:
    function Person(firstName, lastName, age)
    {
        //私有變數:
  = lastName;

        //公用變數:

        this.age = age;

        //方法:

        this.getName = function()
        {      };
        this.SayHello = function()
        {
      firstName + " " + lastName);
        };

    };

   
    var BillGates = new Person("Bill", "s", 53); ) ;

   

    BillGates.SayHello();
    SteveJobs.SayHello();
    alert(BillGates.getName() + " " + BillGates.Billage);

    很顯然,這種模型的類別描述特別象C#語言的描述形式,在一個構造函數裡依序定義了私有成員、公共屬性和可用的方法,顯得非常優雅嘛。特別是「閉包」機制可以模擬對私有成員的保護機制,做得非常漂亮。

    所謂的“閉包”,就是在構造函數體內定義另外的函數作為目標對象的方法函數,而這個對象的方法函數反過來引用外層外層函數體中的臨時變量。這使得 只要目標物件在生存期間始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變數值。儘管最開始的建構函式呼叫已經結束,臨時變數的名稱也都消失 了,但在目標物件的方法內卻始終能引用到該變數的值,而且該值只能通這種方法來存取。即使再次呼叫相同的建構函數,只會產生新物件和方法,新的臨時變數 只是對應新的值,和上次那次的呼叫是各自獨立的。的確很巧妙!

    但前面我們說過,給每個物件設定一份方法是一種很大的浪費。還有,「閉包」這種間接保持變數值的機制,往往會為JavaSript的垃圾回收 器製造難題。特別是遇到物件間複雜的循環引用時,垃圾回收的判斷邏輯非常複雜。無獨有偶,IE瀏覽器早期版本確實存在JavaSript垃圾回收方面的內 存洩漏問題。再加上「閉包」模型在效能測試方面的表現不佳,微軟最終放棄了「閉包」模型,而改用「原型」模型。正所謂「有得必有失」嘛。

    原型模型需要一個建構函數來定義物件的成員,而方法卻依附在該建構函數的原型上。大致寫法如下:
    //定義建構子
    function Person(name)
    {
        函數的prototype上
    Person .prototype.SayHello = function()
    {
        alert("Hello, I'm " + this.name);
   name, salary)
    {
        Person. call(this, name);    //呼叫上層建構子
        this.salary = salary;       //擴充的成員
    };概念
    Employee.prototype = new Person()   //只需要其prototype的方法,而此物件的成員則沒有任何意義!
   
    //子類別方法也定義到建構子之上
    Employee.prototype.ShowMeTheMoney = function()
    { 
   
    var BillGates = new Person("Bill Gates");
    BillGates.SayHello();   
   
    var SteveJobs = new Employee("Steve Jobs", 1234);H約k. Money();
    原型類別模型雖然不能模擬真正的私有變量,而且也要分兩部分來定義類,顯得不怎麼「優雅」。不過,物件間的方法是共享的,不會遇到垃圾回收問題,而且效能優於「閉包」模型。正所謂「有失必有得」嘛。
    在 原型模型中,為了實現類別繼承,必須先將子類別建構子的prototype設定為一個父類別的物件實例。創建這個父類別物件實例的目的就是為 了構成原型鏈,以起到共享上層原型方法作用。但在創建這個實例物件時,上層建構函數也會為它設定物件成員,這些物件成員對於繼承來說是沒有意義的。雖然,我 們也沒有給構造函數傳遞參數,但確實創建了若干沒有用的成員,儘管其值是undefined,這也是一種浪費啊。

    唉!世界上沒有完美的事啊!

原型真諦

    正當我們感概萬分時,天空中一道紅光閃過,祥雲中出現了觀音菩薩。只見她手持玉淨瓶,輕拂翠柳枝,灑下幾滴甘露,頓時讓JavaScript又添新的靈氣。

    觀音灑下的甘露在JavaScript的世界裡凝結成塊,成為了一種稱為「語法甘露」的東西。這種語法甘露可以讓我們寫的程式碼看起來更像物件語言。

    要知道這「文法甘露」為何物,就請君側耳細聽。

    在理解這些語法甘露之前,我們需要重新回顧JavaScript構造物件的過程。

    我們已經知道,以var anObject = new aFunction() 形式建立物件的過程實際上可以分為三步驟:第一步是建立一個新物件;第二步將該物件內建的原型物件設定為建構函數prototype引用的那個原型物件;第三步就是將該物件作為this參數呼叫建構函數,完成成員設定等初始化工作。物件建立之後,物件上的任何存取和操作都只與物件本身及其原型鏈上的那串物件 有關,與建構子再扯不上關係了。換句話說,建構函式只是在創建物件時起到介紹原型物件和初始化物件兩個作用。

    那麼,我們能否自己定義一個對象來當作原型,並在這個原型上描述類,然後將這個原型設置給新創建的對象,將其當作對象的類呢?我們又能否將這個原型中的一個方法當作建構函數,去初始化新建的物件呢?例如,我們定義這樣一個原型物件:
    var Person =  //定義一個物件來作為原型類別
    {
        Create: function(     this.name = name;
            this .age = age;
        },
        SayHello: function()  //定義方法
     
        },
        HowOld: function()  //定義方式
{
            alert(this.name + " is " + this.age + " years old.");
       既有構造函數,又有各種方法。如果可以用某種形式來創建對象,並將對象的內建的原型設置為上面這個“類”對象,不就相當於創建該類的對象了嗎?

    但遺憾的是,我們幾乎無法存取到物件內建的原型屬性!儘管有些瀏覽器可以存取到物件的內建原型,但這樣做的話就只能限定了使用者必須使用那種瀏覽器。這也幾乎不可行。

    那麼,我們可不可以透過一個函數物件來做媒介,利用該函數物件的prototype屬性來中轉這個原型,並用new操作符傳遞給新建的物件呢?

    其實,像這樣的程式碼就可以實現這個目標:

    function anyfunc(){};           //    var BillGates = new anyfunc();  //新建物件的內建原型將是我們所期望的原型物件

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn