JavaScript設計模式與開發實踐 | 03 - 閉包和高階函數

閉包

閉包是指有權訪問另外一個函數做用域中的變量的函數。ajax

建立閉包的常見方式,就是在一個函數內部建立另外一個函數。閉包的造成與變量的做用域以及變量的生存週期有關。編程

變量的做用域

變量的做用域就是指變量的有效範圍。設計模式

當在函數中聲明一個變量時,若是變量前面沒有帶上關鍵字var,這個變量就會成爲全局變量;若是用var關鍵字在函數中聲明變量,這個變量就是局部變量,只有在該函數內部才能訪問到這個變量,在函數外部是訪問不到的。數組

在JavaScript中,函數能夠用來創造函數做用域。在函數裏面能夠看到外面的變量,而在函數外面則沒法看到函數裏面的變量。這是由於當在函數中搜索一個變量的時候,若是該函數內並無聲明這個變量,那麼這次搜索的過程會隨着代碼的執行環境建立的做用域鏈往外層逐層搜索,一直搜索到全局對象。變量的搜索是從內到外的。瀏覽器

var a = 1;

var func1 = function(){
  var b = 2;
  var func2 = function(){
      var c = 3;
      console.log(b);  // 輸出:2
      console.log(c);  // 輸出:1
  }
  func2();
  console.log(c);  // 變量c在函數內部,是局部變量,此時在外部訪問不到。 輸出:Uncaught ReferenceError: c is not defined
};

func1();

變量的生存週期

全局變量的生存週期是永久的,除非咱們主動銷燬這個全局變量。而在函數內用var關鍵字聲明的局部變量,當退出函數時,這些局部變量即失去了它們的價值,會隨着函數調用的結束而被銷燬:安全

var func = function(){
  var a = 1;  // 退出函數後局部變量a將被銷燬
  console.log(a);  // 輸出:1
};

func();

可是,有一種狀況卻跟咱們的推論相反。閉包

var func = function(){
  var a = 1;  //函數外部訪問不到局部變量a,退出函數後,局部變量a被銷燬
  console.log(a);  // 輸出:1
};

func();  
console.log(a);  // 輸出:Uncaught ReferenceError: a is not defined


var func = function(){
  var a = 1;
  return function(){
      a++;
      console.log(a);
  }
};

var f = func();

f();  // 輸出:2
f();  // 輸出:3
f();  // 輸出:4
f();  // 輸出:5

當退出函數後,局部變量a並無消失,而是彷佛一直在某個地方存活着。這是由於當執行 var f = func(); 時,f返回了一個匿名函數的引用,它能夠訪問到func()被調用時產生的環境,而佈局變量a一直處在這個環境裏。既然局部變量所在的環境還能被外界訪問,這個局部變量就有了不被銷燬的理由。在這裏產生了一個閉包結構,局部變量的生命週期被延續了。app

閉包的做用

  • 封裝變量異步

  • 延續局部變量的壽命函數

1. 封裝變量

閉包能夠幫助把一些不須要暴露在全局的變量封裝成「私有變量」。

假設有一個計算乘積的函數:

var cache = {};
var mult = function(){
  var args = Array.prototype.join.call(arguments, ',');
  if(cache[args]){
      return cache[args];
  }
  var a = 1;
  for(var i=0, l=arguments.length; i< l; i++){
      a = a * arguments[i];
  }
  return cache[args] = a;
};

console.log(mult(1,2,3));  // 輸出:6
console.log(mult(1,2,3));  // 輸出:6

咱們看到cache這個變量僅僅在mult函數中被使用,與其讓cache變量跟mult函數一塊兒平行地暴露在全局做用域下,不如把它封閉在mult函數內部,這樣能夠減小頁面中的全局變量,以免這個變量在其餘地方被不當心修改而引起錯誤。

var mult = (function(){
  var cache = {};
  return function(){
      var args = Array.prototype.join.call(arguments, ',');
      if(args in cache){
        return cache[args];
      }
      var a = 1;
      for(var i=0, l=arguments.length; i<l; i++){
        a = a * arguments[i];
      }
      return cache[args] = a;
  }
})();

console.log(mult(1,2,3));  // 輸出:6
console.log(mult(1,2,3));  // 輸出:6

提煉函數是代碼重構中的一種常見技巧。若是在一個大函數中有一些代碼可以獨立出來,就把這些代碼封裝在獨立的小函數裏。獨立出來的小函數有助於代碼服用。

var mult = (function(){
  var cache = {};
  var calculate = function(){
      var a = 1;
      for(var i=0, l=arguments.length; i<l; i++){
        a = a * arguments[i];
      }
      return a;
  };

  return function(){
      var args = Array.prototype.join.call(arguments, ',');
      if(args in cache){
        return cache[args];
      }
      return cache[args] = calculate.apply(null, arguments);
  }
})();

console.log(mult(1,2,3));  // 輸出:6
console.log(mult(1,2,3));  // 輸出:6

2.延續局部變量的壽命
img對象經常使用於進行數據上報,以下:

var report = function(src) {
  var img = new Image();
  img.src = src;
};

report('http://xxx.com/getUserInfo');

一些低版本瀏覽器的實現存在bug,在這些瀏覽器中使用report函數進行數據上報會丟失30%左右的數據,也就是說,report函數並非每一次都成功發起了HTTP請求。丟失數據的緣由是img是report函數中的局部變量,當report函數的調用結束後,img局部變量隨即被銷燬,而此時或許還沒來得及發出HTTP請求,因此這次請求就會丟失掉。

把img變量用閉包封閉起來:

var report =(function(){
  var imgs = [];
  return function(src) {
    var img  = new Image();
    imgs.push(img);
    img.src = src;
  }
})();

閉包和麪向對象設計

過程與數據的結合是形容面向對象中的「對象」時常用的表達。對象以方法的形式包含了過程,而閉包則是在過程當中以環境的形式包含了數據。一般用面對對象思想能實現的功能,用閉包也能實現,反之亦然。

看看這段面向對象寫法的代碼:

var extent = {
  value: 0;
  call: function(){
    this.value++;    
    console.log(this.value);
  }
};

// 做爲對象的方法調用,this指向該對象
extent.call();  // 輸出:1
extent.call();  // 輸出:2
extent.call();  // 輸出:3

換成閉包的寫法以下:

var extent = function(){
  var value = 0;
  return {
      call: function(){
          value++;
          console.log(value);
      }
  }
};

var extent = extent();
extent.call();  // 輸出:1
extent.call();  // 輸出:2
extent.call();  // 輸出:3

閉包與內存管理

局部變量原本應該在函數退出的時候就被解除引用,但若是局部變量被封閉在閉包造成的環境中,那麼這個局部變量就能一直生存下去。從這個意義上看,閉包確實會使一些數據沒法被及時銷燬。使用閉包的一部分緣由是咱們選擇主動把一些變量封閉在閉包中,由於可能在之後還須要使用這些變量,把這些對象放在閉包中和放在全局做用域中,對內存方面的影響是一致的。若是在未來須要回收這些變量,能夠手動把變量設爲null。

使用閉包的同時比較容易形成循環引用,若是閉包的做用域鏈中保存着一些DOM節點,這時候就有可能形成內存泄露。但這自己並不是閉包的問題,也並不是JavaScript的問題。在IE瀏覽器中,因爲BOM和DOM中的對象是使用C++以COM對象的方式實現的,而COM對象的垃圾收集機制採用的是引用計數策略。在基於引用計數策略的垃圾回收機制中,若是兩個對象之間造成了循環引用,那麼這兩個對象都沒法被回收,但循環引用形成的內存泄露在本質上也不是閉包形成的。

若是要解決循環引用帶來的內存泄露問題,咱們只須要把循環引用中的變量設爲null。將變量設爲null,意味着切斷變量與它以前引用的值之間的鏈接。當垃圾收集器下次運行時,就會刪除這些值並回收它們佔用的內存。

高階函數

定義

高階函數是指至少知足下列條件之一的函數:

  • 函數能夠做爲參數被傳遞;

  • 函數能夠做爲返回值輸出。

函數做爲參數傳遞

1. 回調函數

在ajax異步請求的應用中,回調函數的使用很是頻繁。當咱們想在ajax請求返回以後作一些事情,但又不知道請求返回的確切時間時,最多見的方案就是把callback函數看成參數傳入發起的ajax請求的方法中,待請求完成以後執行callback函數:

var getUserInfo = function(){
  $.ajax('http://xxx.com/getUserInfo?' + userId, function(data){
      if(typeof callback === 'function'){
          callback(data);
      }
  });
}

getUserInfo(13157, function(data){
  console.log(data.userName);
});

回調函數的應用不只只在異步請求中,當一個函數不適合執行一些請求時,咱們也能夠把這些請求封裝成一個函數,並把它做爲參數傳遞給另外一個函數,「委託」給另外一個函數來執行。

2. Array.prototype.sort

Array.prototype.sort接受一個函數看成參數,這個函數裏面封裝了數組元素的排序規則。從Array.prototype.sort的使用能夠看到,咱們的目的是對數組進行排序,這是不變的部分;而使用什麼規則去排序,則是可變的部分。把可變的部分封裝在函數參數裏,動態傳入Array.prototype.sort,使Array.prototype.sort方法成爲了一個很是靈活的方法。

// 從小到大排序
console.log(    // 輸出:[1, 3, 4]
    [1, 4, 3].sort(function(a, b){
        return a - b;
    })
);

// 從大到小排序
console.log(  // 輸出:[4, 3, 1]
    [1, 4, 3].sort(function(a, b){
        return b - a;
    })
);

函數做爲返回值輸出

1. 判斷數據的類型

判斷一個數據是不是數組,能夠基於鴨子類型的理念來判斷,好比判斷這個數據有沒有length熟悉,有沒有sort方法或者slice方法。但更好的方式是用Object.prototype.toString來計算。

Object.prototype.toString.call(obj)返回一個字符串,好比Object.prototype.toString.call([1,2,3])老是返回[Object Array],而Object.prototype.toString.call("str")老是返回[Object String]。因此咱們能夠編寫一系列isType函數:

var isType = function(type){
  return function(obj){
      return Object.prototype.toString.call(obj) === '[object ' + type + ']';
  }
};

var isString = isType('String');
var isArray = isType('Array');
var isNumber = isType('Number');

console.log(isArray([1,2,3]));   // 輸出:true

2. getSingle

有一種設計模式叫單例模式,下面是它的例子:

var getSingle = function(fn){
  var ret;
  return function(){
    return ret || (ret = fn.apply(this, arguments));
  };
};

var getScript = getSingle(function(){
  return document.createElement('script');
});

var script1 = getScript();
var script2 = getScript();

console.log(script1 === script2);  // 輸出:true

這個高階函數的例子,既把函數看成參數傳遞,又讓函數執行後返回了另外一個函數。

高階函數實現AOP

AOP(面向切面編程)的主要做用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能一般包括日誌統計、安全控制、異常處理等。把這些功能抽離處理以後,再經過「動態織入」的方式摻入業務邏輯模塊中。這樣作的好處首先是能夠保持業務邏輯模塊的純淨和高內聚性,其次是能夠很方便地複用日誌統計等功能模塊。

在JavaScript中實現AOP,都是指把一個函數「動態織入」到另外一個函數之中,具體的實現技術有不少,這裏咱們經過擴展Function.prototype來實現。

function.prototype.before = function(beforefn){
  var __self = this;  // 保存原函數的引用
  return function(){  // 返回包含了原函數和新函數的「代理」函數
      beforefn.apply(this, arguments);  // 執行新函數,修正this
      return __self.apply(this, arguments);  // 執行原函數
  }
};

Function.prototype.after = function(afterfn){
  var __self = this;
  return function(){
      var ret = __self.apply(this, arguments);
      afterfn.apply(this, arguments);
      return ret;
  }
};

var func = function(){
  console.log(2);
};

func = func.beforefn(function(){
  console.log(1);
}).after(function(){
  console.log(3);
});

func();

這種使用AOP的方式來給函數添加職責,也是JavaScript語言中一種很是特別和巧妙的裝飾者模式實現。

PS:本節內容爲《JavaScript設計模式與開發實踐》第三章 筆記。

相關文章
相關標籤/搜索