JS進階之---做用域,做用域鏈,閉包

1、做用域:前端

  在JavaScript中,咱們能夠將做用域定義爲一套規則,這套規則用來管理引擎如何在當前做用域以及嵌套的子做用域中根據標識符名稱進行變量查找。這裏的標識符,指的是變量名或者函數名。web

  JavaScript中只有全局做用域與函數做用域(由於eval咱們平時開發中幾乎不會用到它,這裏不討論)。編程

  做用域與執行上下文是徹底不一樣的兩個概念。閉包

 

  JavaScript代碼的整個執行過程,分爲兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段做用域規則會肯定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段建立。函數式編程

      

  做用域在編譯階段肯定規則,但是爲何做用域鏈卻在執行階段的建立過程肯定?之因此有這個疑問,是由於你們對做用域和做用域鏈有一個誤解。咱們上面說了,做用域是一套規則,那麼做用域鏈是什麼呢?是這套規則的具體實現。函數

 

 

2、做用域鏈this

  做用域鏈,是由當前環境與上層環境的一系列變量對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。spa

  簡單說就是函數能夠訪問的一系列的做用域中的變量對象。翻譯

var a = 20;

function test() {
    var b = a + 10;

    function innerTest() {
        var c = 10;
        return b + c;
    }

    return innerTest();
}

test();

  在上面的例子中,全局,函數test,函數innerTest的執行上下文前後建立。咱們設定他們的變量對象分別爲VO(global),VO(test), VO(innerTest)。而innerTest的做用域鏈,則同時包含了這三個變量對象,因此innerTest的執行上下文可以下表示。 3d

innerTestEC = { VO: {...}, // 變量對象
    scopeChain: [VO(innerTest), VO(test), VO(global)], // 做用域鏈
    this: {} }

  不少人會誤解爲當前做用域與上層做用域爲包含關係,但其實並非。以最前端爲起點,最末端爲終點的單方向通道我認爲是更加貼切的形容。如圖。

      

 

 

3、閉包

  當函數能夠記住並訪問所在的做用域(全局做用域除外)時,就產生了閉包,即便函數是在當前做用域以外執行。簡單來講,假設函數A在函數B的內部進行定義了,而且當函數A在執行時,訪問了函數B內部的變量對象,那麼B就是一個閉包。

  咱們知道,函數的執行上下文,在執行完畢以後,生命週期結束,那麼該函數的執行上下文就會失去引用。其佔用的內存空間很快就會被垃圾回收器釋放。但是閉包的存在,會阻止這一過程。因此,經過閉包,咱們能夠在其餘的執行上下文中,訪問到函數的內部變量。

var fn = null;

function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(a);
    }
    fn = innnerFoo; // 將 innnerFoo的引用,賦值給全局變量中的fn
}

function bar() {
    fn(); // 此處的保留的innerFoo的引用
}

foo();
bar(); // 2

  在上面的例子中,foo()執行完畢以後,按照常理,其執行環境生命週期會結束,所佔內存被垃圾收集器釋放。可是經過fn = innerFoo,函數innerFoo的引用被保留了下來,複製給了全局變量fn。這個行爲,致使了foo的變量對象,也被保留了下來。因而,函數fn在函數bar內部執行時,依然能夠訪問這個被保留下來的變量對象。因此此刻仍然可以訪問到變量a的值。咱們稱foo爲閉包。

       

 

 

4、 閉包的經常使用場景

  一、延遲函數setTimeout

function fn() {
    console.log('this is test.')
}
var timer = setTimeout(fn, 1000);
console.log(timer);

  執行上面的代碼,變量timer的值,會當即輸出出來,表示setTimeout這個函數自己已經執行完畢了。可是一秒鐘以後,fn纔會被執行。這是爲何?

  按道理來講,既然fn被做爲參數傳入了setTimeout中,那麼fn將會被保存在setTimeout變量對象中,setTimeout執行完畢以後,它的變量對象也就不存在了。但是事實上並非這樣。至少在這一秒鐘的事件裏,它仍然是存在的。這正是由於閉包。

  很顯然,這是在函數的內部實現中,setTimeout經過特殊的方式,保留了fn的引用,讓setTimeout的變量對象,並無在其執行完畢後被垃圾收集器回收。所以setTimeout執行結束後一秒,咱們任然可以執行fn函數。

 

  二、模塊

(function () {
    var a = 10;
    var b = 20;

    function add(num1, num2) {
        var num1 = !!num1 ? num1 : a;
        var num2 = !!num2 ? num2 : b;

        return num1 + num2;
    }

    window.add = add;
})();

add(10, 20);

  上面的例子使用函數自執行的方式,建立了一個模塊。add是模塊對外暴露的一個公共方法。而變量a,b被做爲私有變量。

  在面向對象的開發中,咱們經常須要考慮是將變量做爲私有變量,仍是放在構造函數中的this中,所以理解閉包,以及原型鏈是一個很是重要的事情。

 

  三、柯里化
  在函數式編程中,利用閉包可以實現不少炫酷的功能,柯里化算是其中一種。

 


5、留個問題,驗證本身有沒有理解閉包:

  利用閉包,修改下面的代碼,讓循環輸出的結果依次爲1, 2, 3, 4, 5

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

  5分鐘後。。。

for(var i=1; i<=5; i++){
    (function(i){
        setTimeout(function timer(){
            console.log(i)
        }, i*1000);
    })(i);
}        
相關文章
相關標籤/搜索