1. 程式人生 > >JavaScript之閉包的實現、閉包中的this物件

JavaScript之閉包的實現、閉包中的this物件

閉包

函式物件可以通過作用域鏈關聯起來,函式體內的變數可以儲存在作用域中,這種特性稱“閉包”。

要理解閉包,首先要理解巢狀函式的詞法作用域規則:先看下列一段程式碼:

var a = "Tom"; //全域性變數

function curr () {
    var a = "Bob";  //區域性變數
    function () {
        return a;
    }
    return f(); //將f函式執行的結果返回 Bob
}

curr();


當curr()函式呼叫執行完畢後,返回的是f()函式執行的結果即”Bob“,當將這段程式碼修改之後

var a = "Tom";
function curr () {
    var a = "Bob";
    function f () {
        return a;
    }
    return f; //返回的是這個f函式物件
}

curr()(); //f()


當curr()函式執行完畢後,返回的是f函式物件,後面再跟一個圓括號,就是呼叫f函式。定義curr函式時,建立了一個作用域鏈,呼叫curr函式時,會新建立一個物件用來儲存這個函式的區域性變數和引數,並把這個物件儲存在這個作用域上,函式f也定義在了這個作用域鏈中。當curr()函式執行後,其作用域鏈依然有效,上面還儲存著其區域性變數a,巢狀函式f也還在了這個作用域鏈上,因此此時區域性變數a和f函式還是繫結在一起的。最後執行f函式的結果是"Bob"(引用的變數a就是這個區域性變數)。

詞法作用域規則:JavaScript函式執行的時候用到了作用域鏈,這個作用域鏈在定義函式的時候建立的,巢狀的f函式定義在這個作用域鏈中,其中變數a是區域性變數,無論何時何地執行f函式,這種繫結在執行f時依然有效。



實現閉包:

要理解閉包,首先理解詞法作用域鏈規則函式定義時所建立的作用域鏈到函式執行時依然有效

首先回顧一下作用域鏈:定義函式時就會建立作用域鏈,將作用域鏈看作是物件列表或鏈列表。每次呼叫函式時,就會建立一個新物件用來儲存(定義)區域性變數,並把這個物件新增到作用域鏈中。當函式返回時,就將這個物件從作用域鏈中刪除:

1、如果這個函式沒有定義巢狀的函式,也沒有其它引用指向這個物件,那麼這個物件就會被回收掉。

2、如果這個函式定義了巢狀的函式,每個巢狀的函式都有自己的作用域鏈,並且這個作用域鏈指向一個變數繫結物件:
  • a、如果這些巢狀的函式在函式外部保留了下來,那麼它們也會和所指向的變數繫結物件被當作垃圾回收掉。
  • b、如果 這個巢狀的函式物件 被當作返回值返回或作為某物件的屬性儲存了下來,這時就會有一個外部引用指向這個巢狀的函式物件,它就不會被當作垃圾回收掉,並且它所指向的變數繫結物件也不會被回收掉。

下面舉個經典例子:返回一個函式組成的陣列,它們返回0~9的數
functino consFunc () {
    var arr = [];
    for (var i = 0; i < 10; i ++) {
        arr[i] = function () {
            return i;
        };
    }
    return arr;
}

var a = consFunc(); //呼叫並返回consFunc
console.log(a[5]()); //10 看來沒有返回我們想要的值
上面程式碼建立了10個閉包,並將它們儲存在一個數組中,因為每個閉包都是在同一個函式 呼叫中 定義的,因此它們都引用同一個區域性變數i。當consFunc返回時,變數i的值為10,再使用a[5]()呼叫閉包函式,此時所有閉包函式都引用同一個變數i,但此時的變數i的卻是10,所以陣列中的函式的返回值是同一個值。這不是我們想要的。巢狀的函式不會將作用域中的私有變數複製一份的。


解決方案1:可以建立一個匿名函式讓閉包來”記住“變數i的不同值。

function consFun () {
    var arr = [];
    for (var i = 0; i < 10; i ++) {
        arr[i] = function (num) {
            return function () {
                return num;
            };
        }(i);
    }
    return arr;
}

var a = consFunc();
console.log(a[5]()); //5

在重寫了consFunc函式後,每個函式都會返回各自不同的索引值了。這裡定義了一個匿名函式,並將立即執行這個匿名的結果賦值給陣列。這裡的匿名函式有個引數num,也就是最終要返回的值。在呼叫每個匿名函式(閉包)時,傳入了變數i。由於函式是按值傳遞的,所以就會將變數i的當前值複製給num,在這個閉包函式內部,又建立一個閉包函式作為返回值,於是陣列中存放的是這個返回值,這個返回值是一個函式物件,arr陣列中的每個函式都有自己的num變數的副本,因此可以返回不同的值。


解決方案2:其實可以將上面程式碼修改為更簡潔的方式:

function consFun () {
    var arr = [];
    for (var i = 0; i < 10; i ++) {
        arr[i] = function () { //呼叫匿名函式自身,返回的是值。
            return i; 
        }();
    }
    return arr;
}

var a = consFunc();
console.log(a[5]); //5

當consFunc函式返回時,陣列中的閉包函式已經執行完畢,並將執行的結果儲存在了陣列中。



在閉包中關於this物件


我們知道,this物件是在執行時基於函式的執行環境繫結的:在全域性環境中,this物件就是window物件。而當函式作為某個物件的方法呼叫時,this物件就指向這個物件。匿名函式具有全域性性,this物件通常指向window物件,不過有時候,由於閉包函式書寫方式不同,就沒有那麼明顯。

特別說明:當函式被呼叫時會自動取得兩個屬性:this物件和arguments物件。


var name = "hello window";

var obj = {
    name : "hello obj",


    getName : function () {
        return function () {
            return this.name;
        }
    }
};

console.log(obj.getName()()); //hello window


上例定義了一個全域性變數name和一個物件obj。當obj.getName()方法,返回的是一個匿名函式物件,而匿名函式又返回"this.name",由於getName返回的是一個函式,因此"obj.getName()()"就會立即呼叫getName返回的匿名函式,此時匿名函式是作為全域性物件window的方法呼叫的,因此匿名函式中的this物件指向window物件,以"this.name"就相當於"window.name",所以最後返回的結果是"hello window"。

上述例子在開始建立前,想要的是匿名函式返回obj物件中的name屬性。但匿名函式中的this物件是指向window物件的,無法直接引用到obj物件。我們知道在"obj.getName()"中,getName函式是作為obj物件的方法被呼叫的,那麼其this物件就指向了obj物件,那我們可不可以這樣想:如果能使匿名函式(內部函式)引用到getName函式的this物件的話,那麼間接地不就引用到了obj物件了麼,匿名函式最後返回的不就是想要的"hello obj"麼。


我們知道,函式在被呼叫時會自動新增兩個屬性:this物件和arguments物件。內部函式在搜尋這兩個變數時,只會搜尋到其活動物件為止,因此不可能直接訪問到外部函式的這兩個變數。如果將外部的this物件儲存在一個閉包函式(內部函式)可以訪問到的變數裡,是不是可以說這個閉包函式就可以訪問到外部函式的this物件呢?

var name = "hello window";

var obj = {
    name : "hello obj",


    getName : function () {


        var that = this; //將getName函式的this物件儲存在一個變數裡。
        return function () {
            return that.name; //由閉包函式去訪問這個變數。
        }
    }
};

console.log(obj.getName()()); //hello obj


這樣,將getName函式的this物件儲存在一個變數裡,而這個變數可被閉包函式訪問,當閉包函式搜尋that.name時,就會搜尋到getName函式的this.name,getName函式的this物件是引用到obj物件的(that就相當於此this),即使這個函式返回後,that也是引用到obj物件的,那麼最後閉包函式返回的就是"hello obj"。



注意:閉包函式中的that引用的是obj物件,但其this物件引用的是window物件。

var name = "hello window";

var obj = {
    name : "hello obj",


    getName : function () {


        var that = this; //將getName函式的this物件儲存在一個變數裡。
        return function () {
            return that.name + "-" + this.name; // 此處的this物件是指向window物件的。
        }
    }
};

console.log(obj.getName()()); //hello obj - hello window

也就是說,儘管這樣修改程式碼,這個閉包函式還是作為window物件的方法呼叫的。