JavaScript 閉包及其機制

首先要區分兩個概念,一是匿名函數,一是閉包

所謂匿名函數,就是創建函數沒有給定函數名。經常出現的包括函數表達式,就是定義一個匿名函數,然後將函數賦值給某個變量,而此時這個變量就相當於該函數的函數名,例如:

var sayHi = function(){
    alert("Hi");
};  //注意這個分號
sayHi(); //調用函數
還有一種常用匿名函數的情況是回調函數,如 JQuery 中常用到的:

$("p").click(function(){
    alert("click");
});
此外,還有利用匿名函數作爲某函數的返回值:

function sayNameWithAge(age){
    return function(person){
        if(person.age == age){
            return person.name;
        }
    }
}

那麼,閉包又是怎麼一回事呢?所謂的閉包,其實就是一個函數,而這個函數有一點比較特別,它有權能夠去訪問其他函數作用域的變量。

從定義中我們發現,其實在上面的匿名函數例子中,就存在這樣的閉包。在最後一個例子中,匿名函數訪問了函數 sayNameWithAge 的參數 age,那麼,這個作爲返回值的匿名函數就是一個閉包。

要徹底理解閉包,就必須理解函數調用時的整個機制,這裏從作用域鏈的相關知識來進行講解。

首先看下面的例子:

function sayName(name){
    alert(name);
}

sayName("Jack");
在上面的函數 sayName 被調用的時候,就會創建一個對應的執行環境和作用域鏈,如下圖所示:

當 sayName 函數被調用時,創建了相應的作用域鏈,而作用域中包含兩個引用分別指向兩個對象,其中一個是全局變量對象,這個全局對象是在函數創建的時候就已經創建了,只是在調用函數的時候纔將其複製到作用域鏈中;而另一個就是函數的活動對象,這個對象是在調用函數的時候才創建的。

在函數中訪問一個變量時,就會從作用域中搜索對應名字的變量。

而當函數執行完畢後,函數的活動對象會被銷燬,而全局變量對象卻永遠保存在內存中。

但是,上面所說的都是普通函數的情況,對於閉包而言,又是另外一種情況:

以上面的 sayNameWithAge 函數爲例:

function sayNameWithAge(age){
    return function(person){
        if(person.age == age){
            return person.name;
        }
    }
}
//創建函數
var sayName = sayNameWithAge(18);
//調用函數
var name = sayName({name:"Jack",age:18});
//解除對匿名函數的引用
sayName = null;
當上面的 sayName 函數被調用的時候,產生的作用域鏈如下所示:


當匿名函數被 return 後,它的作用域鏈被創建,並且包含了外部函數的活動對象和全局變量對象,這樣一來,這個匿名函數就可以訪問 sayNameWithAge 函數中定義的所有變量,也就是一個閉包。

這樣的閉包會存在一個問題,就是當 sayNameWithAge 函數執行完畢的時候(JS 的垃圾處理機制大多是標記清除),其活動對象被閉包所引用,所以活動對象並不會被銷燬,只有當匿名函數被銷燬後,sayNameWithAge 的活動對象纔會被銷燬,所以上面的最後一行解除對匿名函數的引用不僅是爲了銷燬閉包的對象,也是爲了銷燬外部函數的活動對象。所以,慎重使用閉包!!!

關於閉包,還有一個需要注意的地方,就是在閉包中訪問其他函數的變量,實際上是因爲閉包的作用域鏈中有指向其他函數的活動對象的引用,而不是閉包自身的活動對象中保存着這些變量。看下面的例子:

function outer(){
    var result = new Array();
    for(var i = 0; i < 5; i ++){
        result[i] = function(){
            return i;
        };
    }
    return result;
}
按照設想,最後 outer 返回的數組各個項中的值應該是與其下標一致的。但是,最後的結果卻是每個項的值都是 5

不難想象,在上面的所有閉包的作用域鏈中,都有一個引用指向了 outer 的活動對象中的參數 i,而且是指向同一個對象。

當 outer 函數執行完畢的時候,i 的值是 5。也就是說,所有閉包中訪問 i 的時候取到的值都是 5

那麼,我們可以通過另一種方法來實現預想的效果:

function outer(){
    var result = new Array();
    for(var i = 0; i < 5; i ++){
        result[i] = (fuction(index){
            return index;
        })(i);
    }
    return result;
}
這裏我們爲匿名函數定義一個參數 index,並在每次循環中立即調用該函數,將 i 的當前值複製給參數 index(注意 JS 中是按值傳遞),並將返回的 index 賦值給 result。

此外,閉包中需要注意的另一個問題是 this 對象。

this 對象在 JS 中是在函數運行時基於函數的執行環境綁定的。而匿名函數的執行環境具有全局性,也就是說,在匿名函數中,this 對象通常指向 window。

var name = "Tom";
var person = {
    name : "Jack",
    sayName : function(){
        return (function(){
            return this.name;
        })();
    }
}
person.sayName(); //Tom
上面在閉包中訪問 this.name,其中的 this 對象並非取得自身或是 person 的 this 對象,而是指向 window。

如果需要在閉包中訪問外部函數的 this 對象,那麼,可以在外部函數中定義一個變量,將 this 對象傳給該變量。

var name = "Tom";
var person = {
    name : "Jack",
    sayName : function(){
        var self = this;
        return (function(){
            return self.name;
        })();
    }
}
person.sayName(); //Jack


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章