JavaScript精粹(4-2)

來源:互聯網
上載者:User
4.9   範圍在程式設計語言中,範圍控制著變數與參數的可見度及生命週期。對程式員來說這是一個重要的協助,因為它減少了名稱衝突,並且提供了自動記憶體管理。大多數使用C語言文法的語言都擁有塊級範圍。在一個代碼塊中(括在一對花括弧中的語句集)定義的所有變數在代碼塊的外部是不可見的。定義在代碼塊中的變數在代碼塊執行結束後會被釋放掉。這是件好事。糟糕的是,儘管代碼塊的文法似乎表現出它支援塊級範圍,但實際上JavaScript並不支援。這個混淆之處可能成為錯誤之源。JavaScript確實有函數範圍。定義在函數中的參數和變數在函數外部是不可見的。但在一個函數中的任何位置定義的變數在該函數中的任何地方都可見(默然說話:我的天呀,真是一個災難。。。。)。很多現代語言都推薦儘可能遲地聲明變數。而用在JavaScript上卻會成為糟糕的建議,因為它缺少塊級範圍。所以,最好的做法是在函數體的頂部聲明函數中可能用到的所有變數。 4.10   閉包只有函數範圍的好處是內建函式可以訪問定義它們的外部函數的參數和變數(除了this和arguments)。這是一件非常好的事情。我們的getElementsByAttribute函數可以工作是因為它聲明了一個results變數,且傳遞給walk_the_DOM的內建函式也可以訪問results變數。一個更有趣的情形是內建函式擁有比它的外部函數更長的生命週期。之前,我們構造了一個myObject對象,它擁有一個value屬性和一個increment方法。假定我們希望保護該值不會被非法更改。與前面直接定義一個對象不同,我們通過調用一個函數的形式去初始化myObject,該函數將返回一個對象。此函數定義了一個value變數。該變數對increment和getValue方法總是可見的,但函數的範圍使得它對其他的程式來說是不可見的。 var myObject=function(){        var value=0;        return {               increment:function(inc){                      value+=typeof inc==='number'?inc:1;               },               getValue:function(){                      return value;               }        } }();我們並沒有把一個函數賦值給myObject,我們是把調用該函數後返回的結果賦值給它(默然說話:注意最後一行的())。該函數返回一個包含兩個方法的對象,並且這些方法繼續享有訪問value變數的特權。本章之前的Quo構造器產生出帶有status屬性和get_status方法的一個對象。但那看起來並不是十分有趣。為什麼要用一個getter方法去訪問本可以直接存取到的屬性呢?如果status是私人屬性時,它才是更有意義的。所以,讓我們定義另一種形式的quo函數來做此事: //建立一個名為quo的建構函式。 //它構造出帶有get_status方法和status私人屬性的一個對象。 var quo=function(status){        return {               get_status:function(){                      return status;               },               set_status:function(st){                      status=st;               }        }; }; //構造一個quo執行個體 var myQuo=quo(“amazed”); document.writeln(myQuo.get_status());這個quo函數被設計成無須在前面加上new來使用,所以名字也沒有首字母大寫(默然說話:當然,你也可以加new,效果是一樣的)。當我們調用quo時,它返回包含get_status方法的一個新對象。該對象的一個引用儲存在myQuo中。即使quo函數已經運行結束,但get_status方法仍然享有訪問status的特權。get_status方法並不是訪問該參數的一個拷貝,它訪問的就是該參數本身。因為該函數可以訪問它被建立時所處的上下文環境。這就被稱為閉包。 //定義一個函數,它設定一個DOM節點為黃色,然後把它漸層為白色 var fade=function(node){        var level=1;        var step=function(){               var hex=level.toString(16);               node.style.backgroundColor='#FFFF' +hex+hex;               if(level<15){                      level+=1;                      setTimeout(step,100);               }        };        step(); }; <body onload=”fade(document.body)”></body>我們調用fade,把document.body作為參數傳遞給它(HTML<body>標籤所建立的節點).fade函數設定level為1。它定義了一個step函數;接著調用step函數,fade函數結束。step函數把fade函數的level變數轉化為16進位字元。接著,它修改fade函數得到的節點的背景色。然後查看fade函數的level變數。如果背景還沒變成白色,那就增大level變數再使用setTimeout讓自己再次運行。step很快被再次調用,這時fade函數早已運行結束,但只要fade的內建函式需要,它的變數就會保留(默然說話:耶!偉大的閉包!!!)。理解內建函式能訪問外部函數的實際變數本身而不是一個副本非常重要,看下面的例子。 //糟糕的例子 //構造一個函數,用錯誤的方式給一個數組中的節點設定事件處理常式。 //當點擊一個節點時,按照預想應該彈出一個對話方塊顯示節點的序號 //但其實所有的事件總是會顯示節點的數目。 var add_the_handlers=function(nodes){        var i;        for(i=0;i<nodes.length;i++){               nodes[i].onclick=function(e){                      alert(i);//因為這裡是直接引用了變數i,而不是副本,所以當點擊節點時,總是顯示迴圈之後i的值               }        } } <body onload="add_the_handlers(document.getElementsByTagName('div'))"> <div style="width:300px;height:300px;border:1px solid black;"></div> <div style="width:300px;height:300px;border:1px solid black;"></div> <div style="width:300px;height:300px;border:1px solid black;"></div> <div style="width:300px;height:300px;border:1px solid black;"></div> </body>add_the_handlers函數目的是給每個事件處理函數一個唯一值(默然說話:即每一次迴圈時i的值,它需要很多個i的副本,每個i值都不一樣),但它直接引用了i,所以每個事件處理函數都得到了迴圈後i最終的值。 //好例子 //構造一個函數,用正確的方式給一個數組中的節點設定事件處理常式。 //你點擊一個節點,將會彈出不同的序號 var add_the_handlers=function(nodes){        var i;        for(i=0;i<nodes.length;i++){               nodes[i].onclick=function(e){                      return function(){                             alert(e);                      };               }(i);        } };  <body onload=" add_the_handlers(document.getElementsByTagName('div'))"> <div style="width:300px;height:300px;border:1px solid black;"></div> <div style="width:300px;height:300px;border:1px solid black;"></div> <div style="width:300px;height:300px;border:1px solid black;"></div> <div style="width:300px;height:300px;border:1px solid black;"></div> </body> </html>現在,我們定義了一個函數並立即傳遞i進去執行,而不是把一個函數賦值給onclick。那個函數將返回一個事件處理函數。這個事件處理函數列印的是e而不是i,這樣就可以避免以上情況(默然說話:中文版的原始碼有誤,中文翻譯也有誤,害我花了半個小時的時間才理解這段文字的本意,為了讓讀者容易理解,我對函數進行了修改) 4.11   回呼函數可以讓不連續事件的處理變得更容易。例如:假定有這麼一個序列,由使用者互動開始,向伺服器發送請求,最終顯示伺服器的響應。最純樸的定法可能會是這樣的: request=prepare_the_request(); response=send_request_synchronously(request); display(response);這種方式的問題在於網路上的同步請將會導致用戶端進入假死狀態。如果網路傳輸或伺服器很慢,響應性的降低將是不可接受的。更好的方式是發起非同步請求,提供一個當伺服器的響應到達時將被調用的回呼函數。這樣用戶端不會被阻塞。 request=prepare_the_request(); send_request_asynchronously(request,function(response){        display(response); })(默然說話:不要試圖運行這兩段代碼,因為這兩段代碼僅僅是用來說明的,屬於虛擬碼) 4.12   模組模組是一個提供介面卻隱藏狀態與實現的函數或對象,我們可以使用函數和閉包來構造模組。通過使用函數去產生模組,我們幾乎可以完全摒棄全域變數的使用,從而緩解這個JavaScript的最為糟糕的特性之一所帶來的影響。舉例來說,假定我們想要給String增加一個deentityify方法。它的任務是尋找字串中的HTML字元實體並替換為它們對應的字元。在一個對象中儲存字元實體的名字和它們對應的字元是有意義的。但我們該在哪裡儲存該對象呢?我們可以把它放到一個全域變數中,但全域變數是魔鬼。我們可以把它定義在該函數中,但是那有運行時的損耗,因為該函數在每次被執行的時候該定義都會被初始化一次。理想的方式是將其放入一個閉包, String.method('deentityify',function(){        //字元對應表,它映射字元的名字到對應的字元        var entity={       quot:'"',       lt:'<',       gt:'>'        };        //返回deentityify方法        return function(){         //這才是deentityify方法。它調用字串的replace方法,         //尋找'&'開頭和';'結束的子字串。如果這些字元可以在字元對應表中找到,         //那麼就將該字元替換為映射表中的值,它用到了一個Regex(參見第七章)         return this.replace(/&([^&;]+);/g,             function(a,b){                var r=entity[b];                return typeof r==='string'?r:a;             }         );        }; }());請注意最後一行,我們用()運演算法立刻調用我們剛剛構造出來的函數。這個調用所建立並返回的函數才是deentityify方法。 document.writeln("&lt; &quot;&gt; ".deentityify());          //輸出<”>模組模式利用了函數範圍和閉包來建立綁定對象與私人成員的關聯,在這個例子中,只有deentityify方法有權訪問字元對應表這個資料對象。模組模式的一般形式是:一個定義了私人變數和函數的函數,利用閉包建立可以訪問私人變數和函數的特權函數;最後返回這個特權函數,或者把它們儲存到一個可訪問到的地方。使用模組模式就可以摒棄全域變數的使用。它促進了資訊隱藏和其他優秀的設計實踐。對於應用程式的封裝,或者構造其他單例對象(譯註:JavaScript的單例就是用定義對象的方法建立對象。它通常作為工具為程式其他部分提供功能支援。),模組模式非常有效。模組模式也可以用來產生安全的對象。假定我們想要構造一個用來產生序號的對象: var serialMaker=function(){    //返回一個用來產生唯一字串的對象。    //唯一字串由兩部分組成:首碼+序號    //該對象包含一個設定首碼的方法,一個設定序號的方法    //和一個產生唯一字串的gensym方法    var prefix='';    var seq=0;    return {     setPrefix:function(p){             prefix=String(p);     },     setSeq:function(s){             seq=s;     },     gensym:function(){             var result=prefix+seq;             seq+=1;             return result;     }    }; };  var seqer=serialMaker(); seqer.setPrefix('Q'); seqer.setSeq(1000); var unique=seqer.gensym();//unique的值是"Q1000" alert(unique);seqer包含的方法都沒有用this或that。因此沒有辦法損害seqer。除非調用對應的方法,否則沒法改變prefix或seq的值。seqer對象是可變的,所以它的方法可能會被替換,但替換後的方法依然不能訪問私人成員。seqer就是一組函數的集合,而且那些函數被授予特權,擁有使用或修改私人狀態的能力。 4.13   級聯有一些方法沒有傳回值。如果我們讓這些方法返回this而不是undefined,就可以啟動級聯。在一個級聯中,我們可以在單獨一條的語句中依次調用同一個對象的很多方法。一個啟用級聯的Ajax類庫可能允許我們以這樣的形式去編碼: //默然說話:這段代碼僅為了說明級聯的概念,無法運行,其實級聯就是Java中的連續打點調用方法的形式 getElement('myBoxDiv'). move(350,150). width(100). height(100). color('red'). border('10px outset'). padding('4px'). appendText('Please stand by'). on('mousedown',function(m){    this.startDrag(m,this.getNinth(m)); }). on('mousemove','drag'). on('mouseup','stopDrag'). later(2000,function(){    this.color('yellow').    setHTML("What hath God wraught?").    slide(400,40,200,200); }). tip('This box is resizeable');在這個例子中,getElement函數產生一個對應於id=”myBoxDiv”的DOM元素並提供了其他功能的對象。該方法允許我們移動元素,修改它的尺寸和樣式,並添加行為。這些方法每一個都返回該對象,所以調用返回的結果可以被下一次調用所用。級聯可以產生出具備很強表現力的介面。它也能協助控制那種構造試圖一次做很多事情的介面的趨勢(默然說話:說實話,我非常不喜歡這樣的編碼,因為這樣編碼易讀性太差。級聯基本上適用於那些一次編碼之後再也不修改的代碼,或者適用於那些你不想讓包括你自己在內的任何人都看不懂的代碼)。 4.14  套用函數也是值,從而我們可以用有趣的方式去操作函數值。套用允許我們將函數與傳遞給它的參數相結合去產生出一個新的函數。 var add1=add.curry(1); document.writeln(add1(6));            //書上寫結果是7,可我實際調試的結果是undefinedadd1是把1傳遞給add函數的curry方法後建立的一個函數。add1函數把1添加到它的參數中。JavaScript並沒有curry方法,但我們可能通過給Function.prototype添加功能來實現: Function.method('curry', function ( ) {     var slice = Array.prototype.slice,         args = slice.apply(arguments),         that = this;     return function ( ) {         return that.apply(null, args.concat(slice.apply(arguments)));     }; });   curry方法通過建立一個閉包,它包括了原始函數和被套用的參數。curry方法返回另一個函數,該函數被調用時,會返回一個結果,這個結果包括了curry方法傳入的參數和自己的參數。它使用了Array的concet方法把它們串連在了一起。由於arguments數組並非一個真正的數組,所以它並沒有concat方法。要避開這個問題,我們必須在兩個arguments數組上都應用數組的slice方法。這樣產生出擁有concat方法的常規數組。 4.15  默記法(memoization)函數可以用對象去記住先前操作的結果,從而能避免無謂的運算。這種最佳化被稱為默記法(memoization:用於加快程式運算速度的一種最佳化技術,原書中文版翻譯為記憶,我在這裡翻譯為默記法)。JavaScript的對象和數組要實現這種最佳化是非常方便的。比如說,我們想要一個遞迴函式來計算Fibonacci。一個Fibonacci數字是之前兩個Fibonacci數字之和。最前面的兩個數字是0和1. var fibonacci=function(n){    return n<2?n:fibonacci(n-1)+fibonacci(n-2); }  for(var i=0;i<=10;i++){    document.writeln('//'+i+':'+fibonacci(i)+'<br />'); } 運行結果: //0:0 //1:1 //2:1 //3:2 //4:3 //5:5 //6:8 //7:13 //8:21 //9:34 //10:55程式可以工作,但fibonacci函數被調用了453次。我們調用了11次,而它自身調用了442次。如果我們讓該函數應用默記法,就可以顯著地減少它的運算量。我們在一個名為memo的數組裡儲存我們的儲存結果,儲存結果可以隱藏在閉包中。當我們的函數被調用時,這個函數首先看是否已經知道儲存結果,如果已經知道,就立即返回這個儲存結果。 var fibonacci=function(){    var memo=[0,1];    var fib=function(n){     var result=memo[n];     if(typeof result!=='number'){             result=fib(n-1)+fib(n-2);             memo[n]=result;     }     return result;    };    return fib; }();這個函數返回同樣的結果,但它只被調用了29次。我們調用了它11次。它自身調用了18次。我們可以把這種形式一般化,編寫一個函數來協助我們構造帶默記法功能的函數。memoizer函數將取得一個初始的memo數組和fundamental函數。它返回一個管理memo儲存和在需要時調用fundamental函數的shell函數。我們傳遞這個shell函數和該函數的參數給fundamental函數: var memoizer=function(memo,fundamental){        var shell=function(n){               var result=memo[n];               if(typeof result!=='number'){                      result=fundamental(shell,n);                      memo[n]=result;               }               return result;        };        return shell; };現在,我們可以使用memoizer來定義fibonacci函數,提供其初始的memo數組和fundamental函數: var fibonacci=memoizer([0,1],function(shell,n){    return shell(n-1)+shell(n-2); });通過設計能產生出其他函數的函數,可以極大減少我們必須要做的工作。例如:要產生一個默記法的階乘函數,我們只須提供基本的階乘公式即可: var factorial=memoizer([1,1],function(shell,n){    return n*shell(n-1); });
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.