javascript閉包,高階函數及防抖節流實現

閉包與高階函數

  • 本文描述了作用域,閉包,內存管理的理解及高階函數面向切面編程,節流防抖函數多種實現方案

作用域

理解作用域:var a = 2,引擎幹了那些活

  • 遇見 var a,編譯器會詢問當前作用域是否存在該變量,存在,忽略;不存在,在當前作用域下,聲明該變量,並命名爲 a;

  • 運行處理 a=2 時,引擎會詢問當前作用域集合是否存在 a 的變量;存在就使用該變量,不存在繼續向外層作用域查找該變量;找到了,將 2 賦值給 a,沒找到,則拋出異常;
    總結: 變量賦值會有兩個動作,當前作用域聲明該變量,查找變量,找到變量進行賦值;

  • LHS 和 RHS 查詢:LHS 可以簡單說是左側查詢,RHS 右側查詢;LHS 查詢,沒找到變量會創建變量,RHS 查詢,沒找到變量拋出異常;

      function(a){
          var b = a;
          return a+b
      }
      var  c = foo(2);
    
      LHS:3
      RHS:4
    

變量作用域:

  • 變量有效範圍。

函數作用域:

  • 函數可以用來創造函數作用域;函數可以獲取外部變量,外部無法獲取函數內部變量;如果函數內部沒有該變量,函數會根據執行環境創造的作用域鏈,逐次向外層去尋找該變量。
  • 下面代碼,也會幫大家理解函數作用域
          var a = 123;
          var b = 'b1'
          var fn = function () {
              var b = 1;
              var fn1 = function(){
                  var c = 'c'
                  console.log(c) // c
                  console.log(b) // 1
              }
              fn1();
              console.log(c) // c is not defined
          }
          fn()
    

變量生存週期

  • 在函數內部,函數調用結束就會被銷燬;函數外部全局變量,需要手動銷燬,不銷燬會一直存在內存中,銷燬的方案:var a = null;

閉包

  • 上面將作用域和變量生存週期簡單的講述了一下,接下來將會談談閉包,閉包跟這些概念非常緊密。

  • 定義:當函數可以記住並訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行;
    實例:不是閉包的例子

    function foo() { var a = 2;
        function bar() {
            console.log( a ); // 2
        }
        bar();
     }
    foo();
    

    實例:閉包效果的實例

     function foo() {
       var a = 2;
        function bar() {
            console.log( a ); // 2
        }
       return  bar;
     }
    var baz = foo();
    baz();
    

    分析: 閉包函數外部作用域,可以訪問函數內部詞法作用域的變量。baz()對該作用域進行引用,這個引用就叫做閉包

  • 爲了更好的理解閉包,下面在寫兩個例子

    實例 1:

     function fo (){
         var a = 2
         function ba(){
             console.log(a)
         }
         bg(ba)
     }
     function bg(fn){
         fn() // 閉包
     }
     fo()
    

    實例 2:

    var bg;
    function fo (){
       var a = 2
       function ba(){
           console.log(a)
       }
      bg = ba;
    }
    function bar(){
       bg() //閉包
    }
     fo();
     bar();
    

    分析: 總而言之,閉包能將函數內部的作用域,傳遞到函數外部作用域,進行引用,都會使用閉包。

閉包與內存管理

  • 經常也會聽說閉包,會造成內存泄漏,這個說法並不正確。內存泄漏,得看根對象裏有多少引用,沒有被根對象引用得變量或者引用類型,都會被標記-清除算法給回收。其實大量得閉包引用被濫用,纔是導致內存得元兇。函數內部得局部變量在函數調用完就會被回收。對於全局得引用變量,則是可以通過引用計數進行垃圾回收。循環引用,造成的內存泄漏,只要將循環引用類型的變量設置爲null,瀏覽器就會通過標記清除算法,進行回收內存。記住一點,閉包並不會造成內存泄漏。
閉包封裝函數
 var fn4 = (function(){
        var cache = {};
        return function(){
            var args = Array.prototype.join.call(arguments, ',')
            if(cache[args]){
                return cache[args]
            }
            var a = 1;
            for(var i = 0;i<arguments.length; i++){
                a = a*arguments[i]
            }
            return cache[args] = a;
        }
    })()

高階函數

  • 高階函數可以說是日常開發中經常用的,其實可以說高階函數是閉包的一種運用。那咱們先來看看高階函數的定義吧;
  • 定義: 函數可以作爲參數被傳遞; 函數可以作爲返回值輸出;這兩個條件滿足其中一個就可以稱爲高階函數。
  • 函數作爲參數,像 Array.prototype.sortArray.prototype.filter等等,都屬於高階函數
  • 函數作爲返回值:示例,數據類型判斷
    var isType = function(type){
          return function(obj){
           
              return Object.prototype.toString.call(obj) === '[object '+type+']'
          }
        }
    
        var isArray = function(obj){
            return isType('Array').call(null, obj);
        }
        var isNumber = function(obj){
            return isType('Number').call(null, obj)
        }
    

高階函數實現AOP編程:

AOP就是面向切面編程,把核心功能無關寫邏輯抽離出來,通過動態植入方式,摻進邏輯模塊中;達到模塊的純淨與高內聚。

1. 示例:

 Function.prototype.before = function(fn){
      var that = this; // function
      return function(){
          fn.apply(this, arguments);
          return that.apply(this,arguments)
      }
  }
  Function.prototype.after = function(fn){
      var that = this;
      return function(){
         var self = that.apply(this, arguments)// 執行原函數,形成鏈式調用
          fn.apply(this, arguments) // 執行摻入函數,修正this
          return self
      }
  }
  var fng1 = function(){
      console.log(1)
  }
  var fng2 = fng1.before(function(){
      console.log(0)
  }).after(function(){
      console.log(2)
  })

高階函數實現柯里化函數:currerying

   var cont = (function(){
      var money = [];
      return function(){
          if(arguments.length === 0){
              var mon = 0;
              for(var i = 0; i<money.length; i++){
                  mon +=money[i]
              }
              return mon
          }else{
              Array.prototype.push.apply(money, arguments)
          }
      }
  })()
  cont(100);
  cont(200);
  console.log(cont())

uncurrying函數實現

Function.prototype.unCurrying = function(){
     var that =this;
     return function(){
         var o = Array.prototype.shift.call(arguments);
         
         return that.apply(o, arguments)
     }
 }
  var push = Array.prototype.push.unCurrying();
  var num =  (function(){
     push(arguments, 4)
     console.log(arguments)
 })(1,2,3)

分析: 上面的代碼是由javascript的之父 Brendan Eich寫的一段代碼,其實上面的一段代碼的效果也可以通過Array.prototype.push.call(arguments, 4)是一樣的結果,關鍵在這種思維方法。
push方法接受兩個參數,num函數的argument和4,Array.prototype.push.unCurrying();表示unCurrying方法複製了數組的push方法;而unCurrerying方法的返回值就是Function對象中的unCurreying方法並且實現了arguments繼承數組的push方法,並且把4中對應argumentsd的值,通過apply的顯性綁定,給添加到第一個arguments的值中去。這個時候閉包中nums對應的arguments
的長度就是4,參數就是1234;

節流與防抖

1. setTimeout節流

var setThrottle =  (function(){
    var throFlag = true; //節流判斷
    return function(fn, time){
        var that = this;
        if(!throFlag){
            return false;
        }
        throFlag = false;
        setTimeout(()=>{
            fn.apply(this, arguments)
            throFlag = true;
        }, time)
    }
})();

2. date時間值節流

const setDateThro = (function(){
    let old = Date.now();
    return function(fn, time){
        if(Date.now()- old >= time){
            fn.apply(this, arguments);
            old = Date.now();
        }
    }
})()

3. 混合節流,最後一次也需要執行該函數

 const componentThro = (function(){
        let old = Date.now();
        let timeout;
        return function(fn, time){
            const args = arguments;
            clearTimeout(timeout)
            if(Date.now()- old > time){
                fn.apply(this, args);
                old = Date.now();
            }else{ // 事件完結後再執行一次
                
                timeout = setTimeout(()=>{
                    fn.apply(this, args);
                }, time)
            }
        }
    })()
        div1.onclick = function(){
            componentThro(()=>{
                console.log('節流')
            }, 1000)
        }

4. 防抖,第一次執行

  var deben = (function(){
        let timeout = undefined;
        let preFlag = true;
        return function(fn, time){
            const args = arguments;
            if(preFlag){ // 第一次執行一次;
                fn.apply(this, args)
            }
            preFlag = false
            clearTimeout(timeout)
            timeout = setTimeout(()=>{
                preFlag = true;
            }, time)
          
        }
    })()
        div1.onclick = function(){
            deben(()=>{
                console.log('防抖')
            }, 1000)
        }

5. 防抖,最後一次點擊執行

 var deben = (function(){
    let timeout = undefined;
    return function(fn, time){
        const args = arguments;
        
        preFlag = false
        clearTimeout(timeout)
        timeout = setTimeout(()=>{
            
            fn.apply(this, args)
            
        }, time)
        
    }
})()
div1.onclick = function(){
        deben(()=>{
            console.log('防抖')
        }, 1000)
}

6. 防抖,第一次和最後一次都需要執行

var deben = (function(){
    let timeout = undefined;
    let preFlag = true;
    return function(fn, time){
        const args = arguments;
        if(preFlag){ // 第一次執行一次;
            fn.apply(this, args)
        }
        preFlag = false
        clearTimeout(timeout)
        timeout = setTimeout(()=>{
            
            fn.apply(this, args)
            preFlag = true;
        }, time)
        
    }
})()
div1.onclick = function(){
    deben(()=>{
        console.log('防抖')
    }, 1000)
}

參考文檔

  • 節流
  • javascript 設計模式與開發實踐
  • 你不知道的javascript
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章