深入理解JavaScript作用域鏈與閉包

QQ羣招募中646258285(招募中,沒幾個人說話),
需要交流的朋友可以直接加我微信( DntBeliv )或QQ( 1121864253 )


作用域鏈

和函數的原型一樣,當創建一個函數之後,也會爲函數對象添加一個屬性[[Scopes]]記錄他的作用域。該屬性時一個棧,應該不可以修改,但是我們可以在調試模式中看到。

[[Scopes]]屬性記錄了當前作用域與活動對象,在函數運行時會向棧內壓入一個元素,標識當前的作用域,等函數運行結束後,會將棧頂的元素彈出,銷燬掉當前的作用域。

此外,當我們討論閉包的時候,[[Scopes]]屬性有一個非常重要的特點,那就是他會存儲調用函數的作用域。舉個例子,A函數內創建了函數B,那麼B的[[Scopes]]就會有A函數作用域。此外,所有函數都會包括global作用域。我們也可以猜到,表示函數運行時作用域的是棧頂元素,而棧頂的屬性會屏蔽掉非棧頂相同屬性。

我們看個例子,因爲無法看到函數運行時的情況,所有這裏看不到函數運行時的作用域。這裏主要看一下單個函數和函數內創建函數的不同。

function createComparisonFuntion(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 compareNames = createComparisonFuntion("name");
var result = compareNames({
    name: "Zachary",
    age: 28
}, {
    name: "Nicholas",
    age: 29
});
compareNames = null;

這裏,compareNames 就是createComparisonFuntion函數內創建的函數,compareNames 的作用域應該包括了reateComparisonFuntion作用域,我們來看一下是不是這樣。

在這裏插入圖片描述

可以看到compareNames包括了許多屬性,其中有我們之前提到的arguments、caller等,這裏主要關注[[Scopes]],可以看到裏面有兩個元素,第一個元素(也即是棧頂元素)就是它的調用函數createComparisonFuntion,第二個元素是全局作用域。因爲函數還沒有被調用,所以函數運行時的作用域還沒有被創建。最後我們再來看一下createComparisonFuntion的情況。

在這裏插入圖片描述

最後給一個完整的作用域鏈的圖:

在這裏插入圖片描述

閉包

閉包是指有權訪問另一個函數作用域中變量的函數。我們利用[[Scopes]]就可以實現閉包。

還是上面那個例子,爲什麼createComparisonFuntion已經銷燬了我們還可以訪問到"name"這個參數?這就是因爲compareNames的[[Scopes]]還引用着這個參數,因爲該部分內存還在使用中,因此不會被垃圾回收機制回收。看下面的證明:

在這裏插入圖片描述

現在來看幾個例子:

function createFunction() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        };
    }
    return result;
}

這段代碼希望創建10個不同i值的函數,但實際上所有的i返回都是10。爲什麼會這樣呢?因爲返回的10個函數訪問的i實際上都是通過閉包訪問的createFunction作用域中的活動對象i,只有這一個變量,當createFunction()調用結束後,i就等於10。

var name = "The window";
var obj = {
    name: "my object",
    getName: function () {
        var that = this;
        return function () {
            return that.name;
        };
    }
};
//實際上寫成console.log(obj.getName()());就行了,爲了便於理解,寫成這個
console.log((obj.getName())());//"my object"

在使用vue的回調時經常用到that = this,一直不知道爲啥,現在清楚了吧,如果直接返回this.name,那麼這個this指的是當前作用域,那麼它的值取決於這個(被返回的)函數的調用對象。而如果是that,則就是通過閉包訪問的前一個函數的作用域。

再來看一個內存泄露的問題:

//element較大,會佔用很大內存,不建議這樣寫
function assignHandler(){
    var element = document.getElementById("someEle");
    element.onclick = function(){
        console.log(element.id);
    };
}
//建議這樣寫
function assignHandler(){
    var element = document.getElementById("someEle");
    var id = element.id;
    element.onclick = function(){
        console.log(id);
    };
    element = null;
}

模仿塊級作用域與私有變量

JavaScript中沒有塊級作用域,看下面這個例子:

for (var i = 0; i < 10; i++) {}
var i;
console.log(i);//10

但是函數類似於塊級作用域,我們可以通過閉包模仿塊級作用域:

//函數聲明後面不能跟圓括號
(function() {for (var i = 0; i < 10; i++) {}})();
console.log(i);//報錯:ReferenceError: i is not defined

進一步,我們可以創建具有私有變量的對象:

//函數表達式後面可以跟圓括號
var dingleton = function () {
    //私有變量和私有函數
    var privateVar = 10;
    function privateFun(){
        return false;
    }

    //創建對象
    var obj = new Object();

    //添加特權/公有屬性和方法
    obj.publicProperty = true;
    obj.publicMethod = function(){
        privateVar++;
        return privateFun();
    }
    //返回這個對象
    return obj;
}();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章