最詳細的JavaScript高級教程(十九)函數表達式和閉包

這一章我們介紹函數表達式,在開始的時候我們會複習到很多之前學過的知識。

定義

我們之前學過函數提升相關的知識,定義的函數會提升,定義的函數作爲變量則不會提升,我們還舉例說過下面的代碼才能達到效果

var condition = false;
var sayHi = null;
if(condition){
    sayHi = function(){
        alert('true');
    }
}else{
    sayHi = function(){
        alert('false');
    }
}

現在,我們給這種函數定義之後賦值給變量的寫法叫函數表達式。

var func = function(arg1, arg2){
    return arg1 + arg2;
}

這種寫法是函數表達式最常見的寫法,但不是唯一的寫法,我們之後還會介紹別的函數表達式的寫法。

遞歸

我們知道遞歸就是自己調用自己,我們也學習了下面這種寫法不好,最好使用arguements.callee()方法以防止函數名稱的改變:

function A(){
    A(); 
}
var B = A;
A = null;
B(); // 報錯,這種方法不好,在函數中調用了調用了函數名造成了耦合
function A(){
    arguements.callee(); 
}
var B = A;
A = null;
B(); // 可以運行,但是嚴格模式不允許使用callee

我們發現這兩種寫法一種不好,好的方法嚴格模式還不讓用,這怎麼辦呢?我們使用下面的辦法:

var func = (function A(){
    A();
}); // 這裏的括號寫不寫都可以,但是寫上更好
var B = func;
func = null;
B(); 

閉包的概念

閉包是有權訪問另一個函數作用域中的變量的函數。

閉包的調用原理

我們之前在函數的學習中,學過函數的調用原理,如果不記得了可以複習一下,我們這裏看看閉包的調用原理

function createComparisonFunction(propertyName){
    return function(obj1, obj2){
        var value1 = obj1[propertyName];
        var value2 = obj2[propertyName];
        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    };
}
var compare = createComparisonFunction('name');
var result = compare({name : 'Nic'}, {name : 'Bob'});
alert(result);
compare = null; 

我們看到實現的效果是createComparisonFunction執行完畢之後,還可以調用compare,compare會記住之前初始化的name值。我們說,一般在函數執行完畢之後,其活動對象就會被銷燬,propertyName只是一個局部變量,爲什麼一直存在了呢?這就是閉包跟普通函數調用的不同。

下面我們來描述一下上面代碼中函數的調用:

  1. 定義了compare,調用了createComparisonFunction,創建了createComparisonFunction的執行環境,以及作用域鏈,創建了createComparisonFunction函數的活動對象,作用域鏈上有兩個對象,一個createComparisonFunction的活動對象,一個全局活動對象。createComparisonFunction的活動對象中propertyName的值爲name
  2. 在createComparisonFunction函數調用完成之後,其執行環境,作用域鏈都被銷燬了,但是由於其返回了一個函數,這個函數的作用域鏈中引用了createComparisonFunction的活動對象,所以其活動對象無法被銷燬。
  3. 當調用compare方法的時候,使用了之前遺留的活動對象,所以name生效了
  4. 在使用完成之後,調用compare = null釋放compare對象,才能徹底釋放遺留的活動對象

其具體的指向如下圖:
在這裏插入圖片描述

注意:

書中寫的是作用域鏈是在函數調用的時候生成的,按照上面的實驗,createComparisonFunction只是返回了函數,其活動對象就無法被銷燬了,這是爲什麼呢?

因爲其定義使用的是函數表達式而非函數定義,在使用函數表達式創建函數的時候,因爲變量名必須有所指向,所以作用域鏈,活動對象就都創建出來了。

閉包中的變量問題

我們看下面的代碼返回什麼:

function createFunctions(){
    var result = new Array();
    for(var i = 0;i < 10; i++){
        result[i] = function(){
            return i;
        }
    }
    return result;
}
for(var i = 0;i < 10; i++){
       alert(createFunctions()[i]()); // 全是10
}

分析一下:

  1. createFunctions返回的是一個函數數組,返回的是一堆函數,但是這一堆函數依賴着同一個createFunctions的活動對象,而i存在於createFunctions的活動對象中,並不存在於每一個返回的函數的活動對象中
  2. 當調用createFunctions()的時候,result數組就被填充完了,填充的過程中,i不斷地變大,直到10。所以不管createFunctions()i中的i是幾,其返回的值都是10

那麼如何返回正確的值呢。其關鍵在如何讓函數返回的i的值存在於各自的活動對象中而不是createFunctions的活動對象中。我們可以使用下面的方法:

function createFunctions(){
    var result = new Array();
    for(var i = 0;i < 10; i++){
        result[i] = function(num){
            return num;
        }(i);
    }
    return result;
}
for(var i = 0;i < 10; i++){
       alert(createFunctions()[i]); // 返回0-10
}

結合上面說的問題,這種方法從根本上解決問題,根本問題是變量在哪個活動對象的問題,通過函數的賦值,我們將i作爲變量傳給了內部函數的活動對象,由於i是基本類型,傳值,所以i被傳進了內部函數的活動對象中。

閉包中的this對象

由於活動對象中的this指向的是調用的環境對象,所以調用閉包的函數,this的指向問題很大。我們分析下面的例子:

var name = 'The Window';
var object = {
    name: 'My Object',

    getNameFunc: function(){
        return function(){
            return this.name;
        }
    }
};
alert(object.getNameFunc()()); // The Window

分析:
this返回的是調用的對象,因爲這裏object.getNameFunc()獲取了內部方法,再加一個(),等於在全局對象上調用了方法,所以指向了全局對象window

而實際上,我們認爲內部函數是在object中被調用的,我們希望this指向object,爲了實現這種情況,我們可以使用that

var name = 'The Window';
var object = {
    name: 'My Object',

    getNameFunc: function(){
        var that = this;
        return function(){
            return that.name;
        }
    }
};
alert(object.getNameFunc()()); // My Object

分析一下:

  1. 在閉包中使用that,其活動對象中沒有that,所以到getNameFunc的活動對象中找這個值,找到了之後使用getNameFunc的this屬性作爲that
  2. 如果想在閉包中訪問getNameFunc的arguements對象,也需要這樣操作

閉包的內存泄漏問題

IE9之前如果閉包中存着一個HTML元素,那麼這個元素不能被銷燬。在IE9之後和其他瀏覽器中沒有問題,我們在這裏不展開說

閉包的用處:模仿塊作用域

在實際的開發中,所有的程序員都在同一個全局作用域下編程,很容易帶來潛在的命名衝突,我們應該儘量少的在全局作用域中添加變量,再者,在全局作用域下添加的閉包如果不即時釋放,會消耗額外的資源。

我們知道其他語言有塊作用域而js沒有,其他的語言在for循環的時候定義了i,for循環結束,i就失效了,而js不會。我們還知道,函數是js中唯一可以實現塊作用域的方法,一個函數中的作用域纔是自己的。這樣我們就有了下面的方法來解決問題,模擬塊作用域。

(function() {
    // 這是一個模擬的塊作用域
    // 這是一個閉包
})();

經過這麼一寫,包起來的就是自己的作用域,在調用完成之後立即釋放。那麼這個是不是一個閉包呢?我們學過,閉包是有權訪問另一個函數作用域變量的函數,我們看,把這段代碼放到任何函數中去,都可以訪問這個函數的作用域變量,所以這是一個閉包

我們來看幾個應用

  function outputNumber(count) {
    (function() {
      for (var i = 0; i < count; i++) {
        alert(i);
      }
    })();
    alert(i); //報錯 i is not defined
  }
  outputNumber(3);

使用這種方法可以:

  1. 限制像全局作用域添加過多的變量和函數,減少命名衝突
  2. 創建私有作用域,開發人員可以使用自己的變量不擔心搞亂全局作用域

在ES6的let出現之後我們不需要這麼模擬了,可以使用真正的塊變量:

{
    let a = 1;
}

閉包的用處:私有變量

  function MyObj(name) {
    this.getName = function() {
      return name;
    };
    this.setName = function(value) {
      name = value;
    };
  }
  var person = new MyObj('wf');
  alert(person.getName()); //wf
  person.setName('s');
  alert(person.getName()); //s

通過對於構造函數中添加get set方法,實現了對於name屬性的封裝。這個實踐實現了私有變量但是這個模式基於構造函數模式,我們之前論證過構造函數模式並不是很好的實現對象的模式,每個實例都會重建get set方法,有額外的資源消耗。所以這個方式並不是很好的方式。

閉包的用處:靜態私有變量

我們之前說了不想每次實例化都重建方法,所以我們很自然的想到之前的辦法,將set get方法寫到構造函數的prototype中,寫道構造函數之外。

我們學了閉包,將這一系列操作寫入一個閉包中只暴露構造函數不失爲一個好辦法。

  (function() {
    var name = ''; //這是一個私有變量
    Person = function(value) {
      name = value;
    };
    Person.prototype.getName = function() {
      return name;
    };
    Person.prototype.setName = function(value) {
      name = value;
    };
  })();
  var person1 = new Person('Nic');
  alert(person1.getName()); //Nic

閉包的用處:模塊模式

模塊模式就是一種爲單例創建私有變量和特權方法。這種方法用於對全局單例對象定義私有變量,並且定義方法操作時候使用

var application = function() {
    var component = new Array(); //定義
    component.push(new BaseComponent()); // 初始化
    return {
      //返回單例對象
      getComponent: function() {
        return component.length;
      },
      registerComponent: function(com) {
        if (typeof com == 'object') {
          component.push(com);
        }
      }
    };
}();

閉包的用處:增強模塊模式

我們看到之前我們講的模塊模式返回的是一個字面量標識的對象,這個對象使用typeof判斷就是個Object,如果我們要求,返回的單例必須是某類型的,這時候我們可以把字面量改爲特定類型的實例,也不影響功能,看下面的的代碼:

var application = (function() {
    var component = new Array(); //定義
    var app = new BaseComponent(); // 要求返回的必須是BaseComponent類型
    app.getComponent = function() {
      return component.length;
    };
    app.registerComponent = function(com) {
      if (typeof com == 'object') {
        component.push(com);
      }
      return app;
    };
})();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章