深刻理解JavaScript中的閉包

閉包沒有想象的那麼簡單前端

閉包的概念在JavaScript中佔據了十分重要的地位,有很多開發者分不清匿名函數和閉包的概念,把它們混爲一談,我但願借這篇文章可以讓你們對閉包有一個清晰的認識。數組

你們都知道變量的做用域有兩種:全局變量和局部變量。在JavaScript中函數內部能夠訪問外部全局變量,而函數外部沒法訪問函數的內部局部變量。安全

上邊這一小段話,看似簡單,其實它是咱們理解閉包最基礎的東西。在下邊的內容中,咱們會對這一現象作出解釋。咱們先來看一個很簡單的例子:閉包

const a = 100;

function f1() {
    console.log(a); // => 100
}

f1();

上邊的代碼中的函數f1打印出了全局變量a的值,這說明函數內部能夠訪問外部全局變量。出於某種目的,或者是爲了安全,或者是想使用私有變量,咱們如今須要訪問函數內部的一個局部變量。咱們先看看下邊的代碼:函數

function f1() {
    const  a = 100;
    console.log(a); // => 100
}

console.log(a);

上邊的代碼會產生一個錯誤,說明咱們沒法在函數外部訪問函數內部的局部變量。爲了解決這個問題,咱們就引出了閉包,看一個使用閉包解決上述問題的例子:this

function f1() {
    const  a = 100;
    return function () {
        console.log(a);
    }
}

f1()();

上邊的代碼是一個很簡答的例子,使用閉包後咱們打印出的結果是100.這正好驗證了上邊說的,使用閉包的目的就是解決函數外部沒法訪問函數內部局部變量這一問題。設計

要完全搞清楚其中的細節,必須從理解函數第一次被調用的時候都會發生什麼入手。3d

當某個函數第一次被調用時,會建立一個執行環境(execution context)和相應的做用域鏈,並把做用域鏈賦值給一個特殊的內部屬性(Scope),而後使用this,arguments和其餘命名參數的值來初始化函數的活動對象(activation object)。但在做用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,直到做爲做用域鏈重點的全局執行環境。指針

上邊的這一段話很是重要,它解釋了函數執行的基本原理,閉包也是函數。你們有可能對上邊的話不太理解。咱們經過一個例子來解釋一下:code

function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

上邊的代碼中,咱們首先定義了一個compare()函數,而後又在全局做用域中調用了它。當第一次調用它的時候,一共建立了如下幾個對象:

  • 建立函數的執行環境,固然該函數的執行環境是全局環境
  • 建立函數的做用域鏈
  • 建立一個包含this,arguments,value1,value2的活動對象

咱們看一個圖:

後臺的每一個執行環境都有一個表示變量的對象---變量對象,全局環境的變量對象始終存在,而想compare()函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。

變量對象本質上是一個對象,他存儲了某些變量值。

其實,在compare()函數建立的時候,就已經建立了一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的Scope屬性中。

這句話說明函數在建立後,其內部就有了一個屬性保存着當前的做用域鏈,上邊說compare()函數的做用域鏈指向全局變量對象,這說明做用域鏈中的每一項指向的都是一個變量對象。

當調用compare()函數時,會爲函數建立一個執行環境,而後經過複製函數的Scope屬性中的對象構建起執行環境的做用域鏈。

這句話說明函數在執行環境中,會新建立一個做用域鏈,這個新建的做用域鏈會把函數建立時的做用域鏈複製過來。

此後,又有一個活動對象(在此做爲變量對象使用)被建立並被推入執行環境做用域鏈的前段。

這句話說明,函數執行後,會他this,arguments,函數的參數,函數內部的局部變量這四個做爲屬性,保存到一個對象中,而後把該對象放到做用域鏈的前段,所以咱們就可以經過做用域鏈訪問到咱們須要的數據。

你們能夠再次回到上邊看看那個圖,因爲compare()函數是在全局環境中建立的,所以在執行的時候,它的做用域鏈只有兩個對象,最前端的0指向了執行時的活動對象,1指向了全局的變量對象。

顯然,做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。

這句話很是重要,這使咱們理解函數調用過程最基本的原理。不管何時在函數中訪問一個變量時,就會從做用域鏈中搜索具備相應名字的變量。通常來說,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域(也就是全局執行環境的變量對象)。可是閉包的狀況又有所不一樣。

咱們再看一個帶有閉包的例子:

function createCompareFunction(propertyName) {
    return function (object1, object2) {
        const value1 = object1[propertyName];
        const value2 = object2[propertyName];
        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else  {
            return 0;
        }
    }
}

const compare = createCompareFunction("name");
const result = compare({name: "James"}, {name: "Bond"});

上邊的代碼實現了按照對象的屬性排序的功能。當咱們在一個函數的內部定義了另外一個函數,那麼在該函數執行時,就會把該函數的活動對象添加到它內部的函數的做用域鏈之中。這也就是爲何compare()函數爲何能訪問createCompareFunction內部參數的緣由。

更爲重要的是,createCompareFunction()函數在執行完畢後,器活動對象也不會被銷燬,由於匿名函數的做用域鏈仍然在引用這個活動對象。換句話說,當createCompareFunction()函數返回後,其執行環境的做用域鏈會被銷燬,但它的活動對象人讓親會留在內存中,直到匿名函數被銷燬以後,createCompareFunction()函數的活動對象纔會被銷燬。

const compare = createCompareFunction("name");
const result = compare({name: "James"}, {name: "Bond"});
compare = null;

這其中關於閉包最終要的問題就是,他內部的做用域鏈中會有一個外部函數的活動對象的引用。

咱們看看上邊代碼執行過程當中發生了什麼:

因爲閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存。過分使用閉包可能會致使內存佔用過多,建議你們只在絕對必要時再考慮使用閉包。

可是閉包在使用不當的狀況下會產生必定的反作用,上文中,咱們反覆提到,閉包只能取得包含函數中任何變量的最後一個值,由於閉包保存的是整個變量對象,而是否是某個特殊的變量。

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

const funcs = createFunctions();
console.log(funcs[2]());

上邊的代碼中,看似createFunctions()函數應該返回一個函數數組,數組中的每一個函數都應該返回本身的索引值,但實際上,每一個函數都返回10。createFunctions()函數返回的閉包中保存的是createFunctions()函數的活動對象,這個活動對象中的其中一個屬性就是i。createFunctions()函數執行完畢後,i變成了10,所以當咱們調用閉包函數的時候,他實際上是去訪問了活動對象中的i。基於這個原理,咱們可使用這種方式:

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

const funcs = createFunctions();
console.log(funcs[2]());

殺精編的例子中,用了兩層閉包,最內層的閉包訪問num,外層的閉包訪問i而且當即執行。

在閉包中使用this對象也可能會致使一些問題。咱們知道,this對象是在運行時基於函數的執行環境綁定的:在全局函數中,this等於window,而當函數被做爲某個對象的方法調用時,this等於那個對象。

匿名函數的執行環境具備全局性,在沒有指定調用對象的前提下,this對象一般指向window》

const name = "The window";
const object = {
    name: "My object",
    getNameFunction: function () {
        return function () {
            return this.name;
        }
    }
};

console.log(object.getNameFunction()());

上邊的代碼在調用了object.getNameFunction()返回了一個函數,而後在調用這個返回的函數,就返回了「The window」。

這裏惟一的問題是,爲何匿名函數沒有取得其包含做用域(或外部做用域)的this對象呢?

前面咱們曾經提到過,每一個函數在被調用時,其活動對象都會自動獲取兩個特殊變量:this和arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變量。

這說明訪問內部函數的參數時,獲得的就是內部函數的參數,而不是其餘值,不然就亂套了。

把外部做用域中的this對象保存在一個閉包可以訪問到的變量中,就可讓閉包訪問該對象了:

const name = "The window";
const object = {
    name: "My object",
    getNameFunction: function () {
        const that = this;
        return function () {
            return that.name;
        }
    }
};

console.log(object.getNameFunction()());

記住,this和arguments這兩個比較特殊,只能訪問自身的活動對象。

在幾種特殊的狀況下,this的值可能會意外的改變:

const name = "The window";
const object = {
    name: "My object",
    getNameFunction: function () {
        return this.name;
    }
};

console.log(object.getNameFunction()); // => "My object"
console.log((object.getNameFunction)()); // => "My object"
console.log((object.getNameFunction = object.getNameFunction)()); // => "The window"

最後一行代碼比較有意思,賦值表達式的結果就是函數,而後調用函數以後就打印出了"The window"。

本篇大部份內容來源於<<JavaScript高級程序設計>>

相關文章
相關標籤/搜索