小菊花課堂之JavaScript做用域與閉包

因爲前段時間項目沒有那麼忙,而後我這人一天不看點啥就很是焦慮,因而二刷《你不知道的JavaScript》,如今讀到閉包,想着看完這一章節,寫點東西也是挺好的,因此有了下面的內容,若有不對的地方,敬請斧正,歡迎探討。閉包

做用域

咱們通常講到閉包,就會談到做用域,那麼做用域又分爲了函數做用域塊級做用域 ,在這裏,咱們簡單的介紹一下這兩種做用域。異步

函數做用域

函數做用域是指,屬於這個函數的所有變量均可以在整個函數的範圍內使用及服用(事實上在嵌套的做用域中也可使用)。
咱們先來看一個例子函數

var a = 2;
function foo() {
    var a = 3;
    console.log(a); // 3
}
foo();
console.log(a); // 2

能夠看到,這種技術雖然能解決一些問題,可是也會致使其餘的問題。首先,必須聲明一個具名函數foo(),意味着foo這個名稱自己「污染」了所在的做用域。其次,必須顯式地(有隱式和顯式的區別,這裏暫且不表)經過函數名(foo())調用這個函數才能運行其中的代碼。
那麼咱們有沒有其餘的辦法呢,繼續往下看。工具

var a = 2
(function foo() {
    var a = 3;
    console.log(a); // 3
})();
console.log(a); // 2

比較一下這兩段代碼。第一段中foo被綁定在所在所用域中,能夠直接經過foo()來調用調用它。第二段中foo被綁定在函數表達式自身的函數中而不是所在做用域中。學習

塊級做用域

在JavaScript中,並不支持塊做用域,可是咱們爲何還要說它,由於它的風格在JS開發中很常見。
在平常的開發或者學習工做中,咱們其實常常能見到相似塊做用域,思考如下代碼:code

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

在for循環的頭部直接定義了變量 i,一般是由於只想在for循環內部的上下文中使用 i,而忽略了 i 會被綁定在外部做用域(函數或全局)中的事實。當使用var時,它寫在哪裏都是同樣的,由於它們最終都會屬於外部做用域。
塊做用域是一個用來對以前的最小受權原則進行擴展的工具,將代碼從在函數中隱藏信息擴展爲在塊中隱藏信息。事件

總結爲:任何聲明在某個做用域內的變量,都將附屬於這個做用域。

閉包

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

繼續思考下面代碼:ip

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

看到了嗎,這就是閉包的效果。
函數bar()的詞法做用域可以訪問foo()的內部做用域。而後將bar()函數自己看成一個值類型進行傳遞。
foo()執行後,其返回值賦值給變量baz並調用baz(),實際上只是經過不一樣的標識符引用調用了內部的函數bar()
咱們知道,JavaScript引擎有垃圾回收器用來釋放再也不使用的內存空間。而閉包卻能阻止這件事情發生。事實上內部做用域依然存在,而沒有被回收。
因爲bar()的聲明位置使它擁有涵蓋foo()內部做用域的閉包,使得該做用域可以一直存在,以供bar()在以後進行引用。
再看下面例子內存

function foo() {
    var a = 2;
    function baz() {
        console.log(a); // 2
    }
    bar(baz);
}
function bar(fn) {
    fn(); // 這是閉包
}
foo();

不管經過何種手段將內部函數傳遞到所在的詞法做用域外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包。作用域

OK,本質上不管什麼時候何地,若是將(訪問它們各自詞法做用域的)函數看成第一級的值類型並處處傳遞,你就能看到閉包了。在定時器、事件監聽器、Ajax請求或其餘異步或同步任務中,只要使用了回調函數,實際上就是在使用閉包。

循環與閉包

先看看最多見的for循環。

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

你以爲最後會輸出什麼,每秒輸出一次,分別輸出1~5?

那就錯啦,實際上,它是會每秒輸出一次,但輸出~對,就是66666。
爲何?
延遲函數的回調會在循環結束時才執行,而循環結束的條件就是i再也不<=5。當定時器運行時,即便每一個迭代中執行的是setTimeout(...,0),全部的回調函數依然是在循環結束後才被執行,因此每次都輸出6。
根據做用域的原理,儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以只有一個i。
知道緣由以後,咱們能夠對代碼進行一些改造,看看有沒有好事發生。

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

Fine,咱們終於改造好了,擁有了更多的詞法做用域。在迭代中使用當即執行函數(IIFE)會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會有一個具備正確值的變量。

到這裏,小菊花課堂之JavaScript閉包的內容就告一段落啦,感謝各位能耐心看到這裏。

此時是0點52分,時候也不早了,該洗洗睡啦。
see u ~ again

相關文章
相關標籤/搜索