JavaScript基礎專題之閉包(四)

定義

MDN 對閉包的定義爲:bash

閉包是指那些可以訪問自由變量的函數。閉包

什麼又是自由變量呢?異步

自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。函數

舉個例子:post

var a = 0; //自由變量

function foo() {
    console.log(a);//訪問自由變量,此時這個變量並非函數參數或者函數的局部變量
}

foo();
複製代碼

foo 函數能夠訪問變量 a,可是 a 既不是 foo 函數的局部變量,也不是 foo 函數的參數,因此咱們說 a 就是自由變量,那麼函數 foo 就造成了一個閉包。ui

因此在《 JavaScript權威指南 》中講到:從技術的角度講,全部的 JavaScript 函數都是閉包。spa

在ECMAScript中,閉包指的是:線程

  1. 從理論角度:全部的函數。由於它們都在建立的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。
  2. 從實踐角度:如下函數纔算是閉包:
    1. 即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
    2. 在代碼中引用了自由變量

接下來就來說講實踐上的閉包。code

常見的閉包問題

如下代碼爲何與預想的輸出不符?隊列

// 代碼1
for (var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i) // 輸出5次5
    }, 0)
}
複製代碼

假設A:由於 setTimeout 這塊的任務直接進入了事件隊列中,因此 i 循環以後i先變成了5,再執行 setTimeoutsetTimeout 中的箭頭函數會保存對i的引用,因此會打印5個5.

// 代碼2
for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i) // 輸出 0,1,2,3,4
    }, 0)
}
複製代碼

假設結論 A 成立,那麼上式應該也是輸出5次5,可是很明顯不是,因此結論A並不徹底正確。

那咱們去掉循環,先寫成最簡單的異步代碼:

function test(a){
    setTimeout(function timer(){
        console.log(a)
    },0)
}
test('hello')
複製代碼

複製代碼執行 testsetTimeouttimer 函數放入了事件隊列,timer 保留着 test 函數的做用域(在函數定義時建立的),test 執行完畢,主線程上沒有其餘任務了,timer 從事件隊列中出隊,執行 timer,執行 console.log ( a ) ,因爲閉包的緣由,a 依然會保留着以前的引用,輸出 'hello'

那咱們在回到題目中,由於兩段代碼中的不一樣只有聲明語句,因此咱們提出假設B :由於在代碼1中,匿名函數保留着外部詞法做用域,i 都是在全局做用域上,代碼2中因爲存在塊做用域,因此它保留着每次循環時i的引用。

// 代碼3
for (var i = 0; i < 5; i++) {
    ((i) => {
        setTimeout(function timer() {
            console.log(i) // 輸出 0,1,2,3,4
        }, 0)
    })(i)
}
複製代碼

複製代碼使用 IIFE 傳遞了變量i給匿名函數,IIFE 產生了一個新做用域,timer中保留對匿名函數中的i的引用,因此會依次輸出。

// 代碼4
for (var i = 0; i < 5; i++) {
    (() => {
        setTimeout(function timer() {
            console.log(i) // 輸出 5個5
        }, 0)
    })()
}
複製代碼

代碼3的區別爲IIFE 沒有給匿名函數傳遞 i,timer 保留的做用域鏈中對i的引用仍是在全局做用域上。

通過以上兩個變體的驗證,因此假設B 成立,即:因爲做用域鏈的變化,閉包中保留的參數引用也發生了變化,輸出的參數也發生了變化。

下例,循環中的每一個迭代器在運行時都會給本身捕獲一個i的副本,可是根據做用域的工做原理,儘管循環中的五個函數分別是在各個迭代器中分別定義的,可是它們都會被封閉在一個共享的全局做用域中,實際上只有一個i,換句話說,i的值在傳入內部函數以前,已經爲 6 了,因此結果每次都會輸出 6 。

for(var i=1; i <= 5; i++){
    setTimeout(function(){
        console.log(i);//6
    },0)
}
複製代碼

解決上面的問題,在每一個循環迭代中都須要一個閉包做用域,下面示例,循環中的每一個迭代器都會生成一個新的做用域。

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

也可使用let解決,let聲明,能夠用來劫持塊做用域,而且在這個塊做用域中生明一個變量。

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

總結

簡單的說:函數 + 自由變量就造成了閉包。其實並非特別複雜,只是咱們須要在引用自由變量的時候當心做用域的變化。

JavaScript基礎專題系列

JavaScript基礎專題之原型與原型鏈(一)

JavaScript基礎專題之執行上下文和執行棧(二)

JavaScript基礎專題之深刻執行上下文(三)

新手寫做,若是有錯誤或者不嚴謹的地方,請大夥給予指正。若是這片文章對你有所幫助或者有所啓發,還請給一個贊,鼓勵一下做者,在此謝過。

相關文章
相關標籤/搜索