學習JavaScript閉包

介紹

在以前的文章中連續介紹了做用域的知識,有了這些知識儲備,咱們就來學習本節的內容做用域閉包。回憶工做這幾年,大量使用JavaScript或多或少也在運用閉包,如今咱們試着從理論角度來討論下閉包。閉包

什麼是閉包?

遇到這種問題,第一時間看mdn文檔。官方文檔以下解釋:函數

閉包是由函數以及建立該函數的詞法環境組合而成。學習

這個描述過於抽象不利於理解先放一邊,咱們先來看一段代碼:ui

function foo() { 
    var a = 1;
    function bar() { 
       console.log( a ); // 1
    }
    bar(); 
}
foo();

複製代碼

這是一段嵌套函數代碼,foo函數中聲名了一個bar函數。根據咱們以前學習做用域相關的知識。詞法做用域是由代碼聲明的位置決定的,foo函數其中有兩個標識符abarbar函數能夠訪問在其外部聲明的變量也就是aspa

再來看另外一段代碼:設計

function foo() { 
    var a = 1;
    function bar() { 
        console.log( a );
    }
    return bar; 
}
var baz = foo();
baz(); // 1

複製代碼

這段代碼直觀的展現了閉包的效果。咱們將bar函數自己看成一個函數對象返回。在運行foo函數後,其返回值(也就是bar函數的引用)賦值給變量baz並調用baz(),實際上就是經過不一樣的標識符引用調用了內部函數bar3d

foo()執行後,一般會期待foo()的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器用來釋放再也不使用的內存空間。因爲看上去foo()的內容不會再被使用,因此很天然地會考慮對其進行回收。code

而閉包的「神奇」之處正是能夠阻止這件事情的發生。事實上內部做用域依然存在,所以沒有被回收。誰在使用這個內部做用域?原來是bar()自己在使用。cdn

bar()所聲明的位置,它擁有涵蓋foo()內部做用域的閉包,使得該做用域可以一直存活,以供bar() 在以後任什麼時候間進行引用。對象

bar() 依然持有對該做用域的引用,而這個引用就叫做閉包。

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用 域以外執行。

循環與閉包

for循環是最多見的例子:

for (var i=1; i<=5; i++) { 
setTimeout( function timer() {
    console.log(i);
    }, i*1000 );
}
複製代碼

咱們對這段代碼的預期結果分別輸出數字1-5,每秒一次,每次一個。但這段代碼在運行時會以每秒一次的頻率輸出五次6。延遲函數的回調會在循環結束時才執行。這顯然不是咱們想要的結果。

咱們試圖假設循環中的每一個迭代在運行時都會給本身「捕獲」一個i的副本。可是根據做用域的工做原理,實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個i

咱們須要更多的閉包做用域,特別是在循環的過程當中每一個迭代都須要一個閉包做用域,例如:

for (var i=1; i<=5; i++) { 
    (function(j) {
        setTimeout( function timer() { 
        console.log( j );
        }, j*1000 );
    })( i );
}
複製代碼

在迭代內使用IIFE會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量供咱們訪問。

ES6中let聲明,能夠用來劫持塊做用域,而且在這個塊做用域中聲明一個變量,例如:

for (let i=1; i<=5; i++) { 
    setTimeout( function timer() {
        console.log(i);
    }, i*1000 );
}
複製代碼

模塊與閉包

咱們利用閉包的強大功能,能夠實現一個JavaScript模塊,例如:

function module() {
    var something = "module";
    var another = [1, 2, 3];
    function doSomething() { 
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething, 
        doAnother: doAnother
    }; 
}
var foo = module(); 
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
複製代碼

首先,module只是一個函數,必需要經過調用它來建立一個模塊實例。若是不執行外部函數,內部做用域和閉包都沒法被建立。

而後module函數返回一個對象。咱們保持內部數據變量是隱藏且私有的狀態。能夠將這個對象類型的返回值看做本質上是模塊的公共API。

這個對象類型的返回值最終被賦值給外部的變量foo,而後就能夠經過它來訪問API中的屬性方法,好比foo.doSomething()

能夠對這個模式進行簡單的 改進來實現單例模式:

var foo = (function module() {
    var something = "module";
    var another = [1, 2, 3];
    function doSomething() { 
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething, 
        doAnother: doAnother
    }; 
})();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
複製代碼

小結

閉包本質也是做用域的產物,閉包的規律也是做用域的規律。本章也是簡單的介紹了一下閉包,更多更深刻的內容仍是來源咱們項目中的代碼。

參考

相關文章
相關標籤/搜索