所謂的javascript高級技巧

Js學的也差很少了,該是來總結一下Js中一些比較高級的智慧結晶了。基於Js的動態性、對象都是易變的、函數是第一對象等等其餘語言所不包含的特性,能夠在使用Js的時候創造出更高效、組織性更好的代碼。下面提到的一些概念,是否是很熟悉:html

分支、惰性實例化、惰性載入函數、單例的兩種模式、享元類、函數綁定(糾正函數一個執行上下文)、函數curry化、高級定時器、保護上下文的構造函數、函數節流、自定義事件……前端

js中的繼承、原型、構造函數這些都是老生常談的了。可是對於構造函數式繼承和原型式繼承的優缺點,仍是有必要了解一下的。原型式繼承的主要優勢就是共享方法和屬性,使得從原型對象中繼承出來的子對象均可以在內存中共享所有的方法和屬性,這若是是在大型的繼承鏈中將會大大的改善性能和減小內存的使用量。node

 

接下來看看上面所羅列的每個所謂的「高級」技巧的具體細節咧:web

  1. 惰性實例化
    惰性實例化所要解決的問題是這樣的:避免了在頁面中js初始化執行的時候就實例化了類,若是在頁面中沒有使用到這個實例化的對象,那麼這就形成了必定的內存的浪費和性能的消耗,那麼若是將一些類的實例化推遲到須要使用它的時候纔開始去實例化,那麼這就避免了剛纔說的問題,作到了「按需供應」,簡單代碼示例以下:後端

    var myNamespace = function(){
      var Configure = function(){
        var privateName = "someone's name";
        var privateReturnName = function(){
          return privateName;
        }
        var privateSetName = function(name){
          privateName = name;
        }
        //返回單例對象
        return {
          setName:function(name){
            privateSetName(name);
          },
          getName:function(){
            return privateReturnName();
          }
        }
      }
      //儲存configure的實例
      var instance;
      return {
        getInstance:function(){
          if(!instance){
            instance = Configure();
          }
          return instance;
        }
      }
    }();
    //使用方法上就須要getInstance這個函數做爲中間量了:
    myNamespace.getInstance().getName();瀏覽器

    上面的就是簡單的惰性實例化的示例,可是有一點缺點就是須要使用中間量來調用內部的Configure函數所返回的對象的方法(固然也可使用變量來儲存myNamespace.getInstance()返回的實例對象)。將上面的代碼稍微修改一下,就能夠只用比較得體的方法來使用內部的方法和屬性:緩存

    //惰性實例化的變體
    var myNamespace2 = function(){
      var Configure = function(){
        var privateName = "someone's name";
        var privateReturnName = function(){
          return privateName;
        }
        var privateSetName = function(name){
          privateName = name;
        }
        //返回單例對象
        return {
          setName:function(name){
            privateSetName(name);
          },
          getName:function(){
            return privateReturnName();
          }
        }
      }
      //儲存configure的實例
      var instance;
      return {
        init:function(){
          //若是不存在實例,就建立單例實例
          if(!instance){
            instance = Configure();
          }
          //將Configure建立的單例
          for(var key in instance){
            if(instance.hasOwnProperty(key)){
              this[key]=instance[key];
            }
          }
          this.init = null;
          return this;
        }
      }
    }();
    //使用方式:
    myNamespace2.init();
    myNamespace2.getName();安全

    上面修改了自執行函數返回的對象的代碼,在獲取Configure函數返回的對象的時候,將該對象的方法賦給myNamespace2,這樣,調用方式就發生了一點改變了。性能優化

  2. 分支
    分支技術解決的一個問題是處理瀏覽器之間兼容性的重複判斷的問題。普通解決瀏覽器之間的兼容性的方式是使用if邏輯來進行特性檢測或者能力檢測,來實現根據瀏覽器不一樣的實現來實現功能上的兼容,但問題是,沒執行一次代碼,可能都須要進行一次瀏覽器兼容性方面的檢測,這個是沒有必要的,可否在代碼初始化執行的時候就檢測瀏覽器的兼容性,在以後的代碼執行過程當中,就無需再進行檢測了呢?答案是有的,分支技術就能夠解決這個問題(一樣,惰性載入函數也能夠實現Lazy Definied,這個在後面將會講到),下面以聲明一個XMLHttpRequest實例對象爲例子:服務器

    //分支
    var XHR= function(){
      var standard = {
        createXHR : function(){
          return new XMLHttpRequest();
        }
      }
      var newActionXObject = {
        createXHR : function(){
          return new ActionXObject("Msxml2.XMLHTTP");
        }
      }
      var oldActionXObject = {
        createXHR : function(){
          return new ActionXObject("Microsoft.XMLHTTP");
        }
      }
      if(standard.createXHR()){
        return standard;
      }else{
        try{
          newActionXObject.createXHR();
          return newActionXObject;
        }catch(o){
          oldActionXObject.createXHR();
          return oldActionXObject;
        }
      }
    }();

    從上面的例子能夠看出,分支的原理就是:聲明幾個不一樣名稱的對象,可是給這些對象都聲明一個名稱相同的方法(這個就是關鍵),並給這些來自於不一樣的對象可是擁有相同的方法進行瀏覽器之間各自的實現,接着就開始進行一次瀏覽器檢測,並通過瀏覽器檢測的結果來決定返回哪個對象,這樣不論返回的是哪個對象,最後名稱相同的方法都做爲了對外一致的接口。

    這個是在Javascript運行期期間進行動態檢測,並將檢測的結果返回賦值給其餘的對象,並提供相同的接口,這樣儲存的對象就可使用名稱相同的接口了。其實,惰性載入函數跟分支在原理是很是相近的,只是在代碼實現方面有差別而已。

  3. 惰性載入函數
    惰性載入函數就是英文中傳說的「Lazy Defined」,它的主要解決的問題也是爲了處理兼容性。原理跟分支相似,下面是簡單的代碼示例:

    var addEvent = function(el,type,handle){
      addEvent = el.addEventListener ? function(el,type,handle){
        el.addEventListener(type,handle,false);
      }:function(el,type,handle){
        el.attachEvent("on"+type,handle);
      };
      //在第一次執行addEvent函數時,修改了addEvent函數以後,必須執行一次。
      addEvent(el,type,handle);
    }

    從代碼上看,惰性載入函數也是在函數內部改變自身的一種方式,這樣以後,當重複執行的時候,就不會再進行兼容性方面的檢測了。

  4. 單例的兩種模式
    單例模式是家喻戶曉的了,也是當前最流行的一種編寫方式,註明的模塊模式的編寫方式也是從這個思想中衍生出來的。單例模式有兩種方式:一種是所謂的「門戶大開型」,另一種就是使用閉包來建立私有屬性和私有方法的方式。第一種方式跟構造函數的「門戶大開型」是一個樣的,聲明的方法和屬性對外都是開放的,能夠經過實例來調用。可是使用閉包來實現的單例模式,能夠在一個「封閉」的做用域內聲明一些不爲外部所調用的私有屬性和私有方法,並且還有一個很主要的功能,就是私有屬性能夠做爲一個「數據存儲器」,在閉包內聲明的方法均可以訪問這些私有屬性,可是外部不可訪問。那麼重複添加、修改、刪除這些私有屬性所儲存的數據是安全的,不受外部其餘程序的影響。

  5. 享元類
    顧名思義,「享」、「元」。就是共享通用的方法和屬性,將差別比較大的類中相同功能的方法集中到一個類中聲明,這樣須要這些方法的類就能夠直接從享元類中進行擴展,這樣使得這樣通用的方法只須要聲明一遍就好了。這個從代碼的大小、質量來講仍是有必定的效益的。

  6. 函數綁定
    函數綁定就是爲了糾正函數的執行上下文,特別是函數中帶有this關鍵字的時候,這點尤爲顯得重要,稍微不當心,使得函數的執行上下文發生了跟預期的不一樣的改變,致使了代碼執行上的錯誤(有時候也不會出現錯誤,這樣調試起來,會很變態)。對於這個問題,bind函數是再熟悉不過的了,bind函數的功能就是提供一個可選的執行上下文傳遞給函數,而且在bind函數內部返回一個函數,來糾正在函數調用上出現的執行上下文發生的變化。最容易出現的錯誤就是回調函數和事件處理程序一塊兒使用了,下面是摘自《Javascript高級程序設計第二版》的一個示例:

    var handler = {
      message:"Event handler",
      handlerClick:function(e){
        alert(this.message);
      }
    }
    var btn = document.getElementById("my-btn");
    //這句就形成了回調函數執行上下文的改變了
    EventUtil.addHandler(btn,"click",handler.handlerClick);

    解決的辦法之一,就是糾正一下handler.handlerClick執行的上下文環境,改成:

    //這樣運行的很好
    EventUtil.addHandler(btn,"click",function(e){
      handler.handlerClick(e);
    });

    上面就很好的糾正了回調函數的執行上下文了。並且,也可使用傳說中的bind函數來解決:

    var bind = function(fn,context){
      return function(){
        return fn.apply(context || this,arguments);
      }
    }
    EventUtil.addHandler(btn,"click",bind(handler.handlerClick));// So Good!

  7. 函數curry化
    函數curry化的主要功能就是提供了強大的動態函數建立的功能。經過調用另外一個函數併爲它傳入要curry的函數和必要的參數。說白點就是利用已有的函數,再建立一個動態的函數,該動態的函數內部仍是經過該已有的函數來發生做用,只是傳入更多的參數來簡化函數的參數方面的調用。具體示例:

    //curry function
    function curry(fn){
      var args = [].slice.call(arguments,1); //這個就至關於一個存儲器了。
      return function(){
        return fn.apply(null,args.concat([].slice.call(arguments,0)));
      }
    }
    //Usage:
    function add(num1,num2){
      return num1+num2;
    }
    var newAdd = curry(add,5);
    alert(newAdd(6));

    在curry函數的內部,私有變量args就至關於一個存儲器,來暫時的存儲在curry函數調用的時候所傳遞的參數值,這樣跟後面的動態建立的函數調用的時候的參數合併,並執行,就獲得了同樣的效果了。

  8. 高級定時器
    提到定時器,無非就是利用setTimeout/setInterval了。但問題是定時器並非至關於新開一個線程來執行js程序,也不會說是在指定的時間間隔內就會必定執行。指定的時間間隔表示什麼時候將定時器的代碼添加到瀏覽器的執行隊列,而不是合適實際執行代碼。對此,就有這樣的一個問題了:若是代碼執行時間超過了定時器指定的時間間隔,那麼在指定的時間裏代碼仍是加入的執行隊列,可是並無執行,這樣就會形成了無心義的代碼執行,這也是使用setInterval的弊端。爲此,使用setTimeout才能更好的避免這個問題,在代碼自己中執行完畢了,再經過setTimeout來從新設定定時器,把代碼加入到執行隊列。好比:

    setTimeout(function(){
      //many code here...
      setTimeout(arguments.callee,100); //Key
    },100);

    固然了,定時器還有不少其餘的技巧和實際做用,看需求而定,更詳細的解釋能夠查看《Javascript高級程序設計第二版》(第467頁)。

  9. 保護上下文的構造函數
    這個主要是避免構造函數在沒有使用new來實例化的時候,內部的this指向錯誤問題。一般沒有使用new的話,this通常執行window去了,所以形成了執行錯誤,給代碼帶來了災難。使用下面的方式就能夠避免這個問題:

    function myClass(name,size){
      if(this instanceof myClass){ //Key,使用instanceof來檢測當前實例是不是myClass的實例化對象
        this.name = name;
        this.size = size;
      }else{
        return new myClass(name,size);
      }
    }

    可是上面經過instanceof的方式,給繼承形成了必定的困擾,由於子類並非myClass的實例對象,因此會出現屬性和方法沒法被繼承的方式。在說解決辦法以前,先來了解一下instanceof操做符的原理:它首先會檢測對象當前的原型是否指向右邊的構造函數,若是找不到,就會往上一級的原型去查找,直到找到爲止,並返回true,不然就返回false。

    基於上面的instanceof的原理,在繼承的時候,就能夠給子類的prototype原型賦於一個父類的實例化對象就好了,這樣就能夠在子類繼承的時候繞過instanceof的檢測。

  10. 函數節流

    函數節流函數節流解決的問題是一些代碼(特別是事件)在無間斷的執行,這嚴重的影響了瀏覽器的性能,再沒有給它設定間斷來執行的話,可能形成瀏覽器反應速度變慢或者直接就崩潰了。好比:resize事件、mousemove、mouseover、mouseout等等事件。

    這個時候,就能夠加入定時器的功能了,將事件進行「節流」,便是:在事件觸發的時候,設定一個定時器來執行事件處理程序,這樣能夠很大的程度上緩解瀏覽器的負擔,又緩衝的餘地去更新頁面。具體的實例能夠查看支付寶中部「導購場景」的導航:http://life.alipay.com/?src=life_alipay_index_big,以及噹噹網首頁左邊的導航欄:http://www.dangdang.com/等等,這些都是爲了解決mouseover和mouseout移動過快的時候加大瀏覽器處理的負擔,特別是在涉及到有Ajax調用,並且Ajax調用是麼有緩存的狀況下,給服務器也形成了很大的負擔。爲此,函數節流就派上用場了。好比簡單的示例以下(出自本人寫的:http://www.ilovejs.net/lab/tween/tweener_tab_modify.html此連接貌似無論用)):

    oTrigger.onmouseover=function(e){
      //若是上一個定時器尚未執行,則先清除掉定時器
      oContainer.autoTimeoutId && clearTimeout(oContainer.autoTimeoutId);
      e = e || window.event;
      var target = e.target || e.srcElement;
      if((/li$/i).test(target.nodeName)){
        oContainer.timeoutId = setTimeout(function(){
          addTweenForContainer(oContainer,oTrigger,target);
        },300);
      }
    }

  11. 自定義事件

    首先要說的是,這裏並非說自定義事件能夠真的自定義跟mouseout、click等同樣性質的「事件」。這裏的自定義事件在執行的時候仍是須要依賴已有的鍵盤、鼠標、HTML等事件來執行,或者又其餘函數「觸發」執行,這裏的「觸發」是指直接調用自定義事件中聲明的某個接口方法,來輪詢的執行所有相關的添加到自定義事件中的函數。

    自定義事件內部有一個「事件」存儲器,根據添加的事件的類型的不一樣,來儲存各種的事件執行函數,這樣再出發這類事件的時候,就輪詢執行添加到該類型下的函數。「自定義事件背後的概念是建立一個管理事件的對象,讓其餘對象監聽那些事件」來自《Javascript高級程序設計第二版》的解釋。基於自定義事件的原理,能夠想象自定義事件不少時候是用於「訂閱—發佈—接收」性質的功能。

文章寫的有點多了,可是上面介紹的Javascript高級技巧還遠不止這些,特別是在Ajax方面的一些模式和技巧都尚未介紹,況且是客戶端和服務端結合的一些技巧(好比壓縮、Minify、服務器「推技術」等等)。更多的有待之後瞭解了介紹一二。

在前端基本技術方面,Javascript、HTML、CSS等你們都是已經掌握的差很少了,可是利用這些已有的基本技術,可否創造出不通常的應用和模式呢?這個纔是在掌握了基本的能力以後接下來須要掌握的,好比下面是本人在天天晚上睡覺以前所總結的幾點:

  1. 學會重構代碼的技術,有計劃性的重構下本身以前所編寫過的一些代碼,增強本身掌控代碼的能力。

  2. Code review。這裏說的review,並非指我的,而是對團隊來講的,一我的編寫的代碼的想象空間有限,若是在本身編寫代碼完成以後,邀請其餘團隊內的夥伴來查看你的代碼,及時發現問題以及提出更好的解決方案,這也不失爲一種即時重構的方式,提升代碼的質量。

  3. 編寫具體的功能代碼以前,首先設計代碼、規劃代碼、組織代碼的模式。

  4. 在代碼的質量、性能、大小之間能做出合理的權衡。

  5. 編寫閱讀性良好、一目瞭然、擴展性、可維護性良好、重複利用的代碼,也是一門藝術。

  6. 關注web前端的性能優化,包括Javascript、HTML、CSS、客戶端、服務端、前端、後端等總體性的優化。

  7. 最後一點或許也是最重要的:善於總結。這點比上面的任何一點都來的重要,由於上面的每一點都是出自這點的積累。

上面我總結的幾點,也是後期本身要着重提升的能力,固然了,在實際的編碼方面,還有不少的東西還須要去挖掘和了解。繼續革命吧,將互聯網革命進行到底……

相關文章
相關標籤/搜索