原文地址: medium.com/dailyjs/i-n…
譯文地址:github.com/xiao-T/note…
本文版權歸原做者全部,翻譯僅用於學習。javascript
正如標題所述,JavaScript 閉包對我來講一直是一個謎。爲此,我讀過不少[文章]((en.wikipedia.org/wiki/Closur…),工做中我也用過閉包,有些時候,我甚至都不知道使用了閉包。java
最近,我和一些人討論了一下,他們真正的點醒了我。在這篇文章中,我將會嘗試解釋一下閉包。首先,我要感謝一下 CodeSmith 和他們的JavaScript 的課程。git
爲了理解閉包,有些概念很是重要。其中之一就是執行上下文。github
這篇文章很好的解釋了什麼是執行上下文。如下是引用:數組
當執行 JavaScript 代碼時,執行環境很是重要,而且會按照如下狀況計算:閉包
全局代碼 — 當代碼第一次執行時,默認的執行環境app
函數代碼 — 在函數體內執行的代碼函數
執行上下文
其實就是當前代碼執行的環境/做用域。學習
換句話說,程序開始時,是在全局的執行上下文中。有些變量是在全局上下文中被聲明定義的。咱們稱之爲全局變量。當程序在函數中執行,會發生什麼呢?會有如下幾步:ui
函數什麼時候結束?當遇到 return
語句或者遇到閉合的大括號 }
。當函數結束時,會依次發生如下狀況:
return
語句,默認,將會返回 undefined
。解釋閉包以前,咱們先看一下如下的代碼片斷。它看起很是簡單直接,任何人都知道會發生什麼。
1: let a = 3
2: function addTwo(x) {
3: let ret = x + 2
4: return ret
5: }
6: let b = addTwo(a)
7: console.log(b)
複製代碼
爲了理解 JavaScript 引擎是如何工做的,咱們來逐步分析一下:
在第一行中,咱們在全局執行上下文中聲明瞭一個變量 a
並賦值數字 3
。
接下來會變得棘手。第二行到第五行是一個總體。發生了什麼呢?在全局執行上下文中咱們聲明瞭一個名爲 addTwo
的新變量。咱們給它分配什麼呢?函數定義。兩個大括號 { }
任何代碼都會分配給 addTwo
。函數內部的代碼,這時並不會被計算,也不會執行,只是存儲在一個變量中以便未來使用。
如今,咱們來到了第六行。看起很是簡單,可是,這裏包含了不少東西。首先,在全局執行上下文中,咱們聲明瞭一個新的變量 b
。變量聲明的同時,也會賦值 undefined
。
接下來,仍然是第六行,咱們看到有一個賦值操做符。這時,咱們才真正賦值給變量 b
。接下來,咱們看到函數被調用了。當你看到一個變量後面跟着一個圓扣號 ()
,那表明着函數調用執行。如前所述,每一個函數都會返回一些值(值、對象或者 undefined
)。無論,函數返回什麼都會賦值給變量 b
。
可是,首先咱們須要調用函數 addTwo
。JavaScript 將會在全局執行上下文中查找一個名爲 addTwo
的變量。是的,它找到了,在第二步定義的(第二行到第五行)。你看,變量 addTwo
是一個函數。注意,變量 a
做爲一個參數傳遞給了函數。JavaScript 會在全局執行上下文中搜索變量 a
並找到它,發現它的值是 3
,而後,數字 3
就傳遞給了函數。準備開始執行函數。
如今,執行上下文發生了改變。一個新的本地執行上下文被建立,咱們叫它 「addTwo 執行上下文」。這個執行上下文被壓入到調用棧。在本地執行上下文中咱們首先要作什麼呢?
你可能會說:「一個新的變量 ret
在本地執行上下文中被聲明瞭」。這不對,正確的答案是,首先,咱們須要看一下函數的參數。在本地執行上下文中聲明瞭一個新的變量 x
。而後,因爲數字 3
作爲參數傳遞給了函數,那麼,變量 x
就的值就變成了 3
。
下一步:本地執行上下文聲明瞭新的變量 ret
。它的值是 undefined
(第三行)
仍舊是第三行,須要執行加法。首先,咱們須要用到 x
的值。JavaScript 將會查找變量 x
。首先,它會在本地執行上下文中查找。並且,找到了,它的值是 3
。第二個操做數是數字 2
。二者相加以後的結果(5
)將會賦值給變量 ret
。
第四行。咱們會返回變量 ret
。另外,根據本地執行上下文的內容得知 ret
的值是 5
。函數將會返回數字 5
。這時,函數結束。
函數在第四到第五行結束。本地執行上下文也隨之被銷燬。變量 x
和 ret
同時被抹除。它們將會消失。調用棧也會彈出響應的上下文,返回值將會返回到調用上下文。在這個案例中,調用棧就是全局執行上下文,這是由於,函數 addTwo
是在全局執行上下文中被調用的。
如今,咱們從新回到第四步。返回值(數字5
)賦值給了變量 b
。咱們仍舊在程序的第六行。
我不用詳解介紹,在第七行,變量 b
的值被輸出到了控制檯。它是數字 5
。
爲了解釋一個簡單的程序費了很多口舌,然而,咱們尚未真正的講道閉包。我保證我會的。首先,咱們須要繞個彎路。
咱們須要理解什麼是詞法做用域。看下面的代碼。
1: let val1 = 2
2: function multiplyThis(n) {
3: let ret = n * val1
4: return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)
複製代碼
想法是:咱們在本地執行上下文和全局執行上下文都有變量。JavaScript 中比較難理解是:如何查找變量。若是,在本地執行上下文中找不到,將會在自身的調用上下文中繼續查找。若是,仍是沒有找到。重複以上的動做,直到查到全局執行上下文。(若是,仍舊沒有找到,就會返回 undefined
)。根據這個規則,上面的示例就很清晰了。若是,你清楚做用域是如何工做的,你能夠跳過這部分。
在全局執行上下文中聲明一個變量 val1
,而後,給它賦值數字 2
第 2 - 5 行。聲明瞭一個新變量 multiplyThis
,而後,定義了一個函數
第 6 行。在全局執行上下文聲明瞭一個變量 multiplied
在全局執行上下文中找到變量 multiplyThis
,並作爲一個函數執行。而後,把數字 6
作爲參數傳遞給函數
函數被調用 = 新的執行上下文。建立新的本地執行上下文
在本地執行上下文中,聲明瞭變量 n
並賦值了數字 6
第 3 行。聲明瞭變量 ret
第 3 行。變量 n
和 vall
兩個數的相乘。在本地執行上下文中查找變量 n
。咱們在第 6 行聲明瞭這個變量。它的值是數字 6
。本地上下文中沒有找到變量 vall
。須要檢測調用上下文。由於,調用上下文是全局上下文。咱們須要在全局上下文中查找 vall
。很好,找到了。它在第 1 行被定義的。它的值是數字 2
。
第 3 行。兩個數相差,而後賦值給變量 ret
。6 * 2 = 12。ret
的值是 12
。
返回 ret
的值。隨之本地上下文也被銷燬,同時銷燬的還有變量 ret
和 n
。變量 vall
並不會被銷燬,由於它是全局上下文的一部分。
回到第 6 行。在調用上下文中,數字 12
被複制給變量 multiplied
。
最後的第 7 行,咱們在控制檯中打印了變量 multiplied
的值
經過這個示例,咱們能夠知道函數能夠訪問調用上下文中的變量。這種現象的正式名稱就叫作詞法做用域。
第一個示例中函數 addTwo
返回了一個數字。早前,咱們也說過函數能夠返回任何類型。咱們來看一個函數返回函數的示例,這對於理解閉包相當重要。下面的演示,咱們會一點點分析它。
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
複製代碼
咱們開始一步步的分析下代碼。
第 1 行。咱們在全局上下文中聲明瞭變量 val
,而且賦值數字 7
給變量
第 2 - 8 行。在全局上下文中咱們聲明瞭一個名爲 createAdder
的函數。第 3 - 7 行就是函數的具體定義。和以前同樣,這個時候,咱們並不會執行函數。咱們只是把函數賦值給一個變量(createAdder
)
第 9 行。在全局上下文中,咱們聲明瞭新的變量 adder
。同時,它的值是 undefined
仍是第 9 行。咱們看到了一個圓括號()
;表明着咱們須要調用函數。咱們在全局上下文中搜索找了到名爲 createAdder
的變量。它是在第 2 步建立的。好,咱們來調用它。
調用函數。如今,咱們回到第 2 行。一個新的上下文被建立。在新的上下文中咱們建立了本地變量。同時,引擎也會把新的上下文壓入到調用棧。這個函數沒有參數,咱們直接看它的內部。
第 3 - 6 行。咱們又聲明瞭一個新的函數。在本地上下文中咱們建立變量 addNumber
。這個很重要。addNumber
只在本地上下文中有效。在本地上下文中咱們定義了一個函數並命名爲 addNumber
如今,我來到第 7 行。咱們返回了變量 addNumber
。引擎會查找變量 addNumber
,固然也會找到它。它是一個函數。好,函數能夠返回任何東西,包括函數。所以,咱們返回了 addNumbers
的函數體。在第 4 - 5 行就是函數的具體定義。同時,咱們也把本地上下文從調用棧中移除。
return
以後,本地上下文也隨之銷燬。變量 addNumbers
也不存在了。可是,函數的定義仍然存在,它經過 retrun 語句,並賦值給了變量 adder
;這個變量,咱們是在第 3 步建立的。
來到第 10 行。在全局上下文中,咱們定義了新的變量 sum
。並分配了一個臨時的值 undefined
接下來,咱們須要執行函數。哪個函數呢?就是名爲 adder
的函數。咱們在全局上下文中查找它,能夠保證必定能找到它。這個函數須要兩個參數
咱們獲得了兩個參數,並把它們傳遞了函數。第一個是變量 val
,咱們在第 1 步定義的,它的值是數字 7
,第二個參數是數字 8
如今,咱們來調用函數。這個函數是在第 3 - 5 行被定義的。一個新的本地上下文被建立。在這個上下文中有兩個新的變量:a
和 b
。它們的值分別是 7
和 8
,這就是咱們在上一步傳遞給函數的。
第 4 行。名爲 ret
的變量被聲明。它只存在本地上下文中。
第 4 行。咱們把變量 a
和 b
相加。相加後的結果(15
)賦值給了變量 ret
變量 ret
經過函數返回。隨之,與之相關的本地上下文被銷燬,也從調用棧中被移除,變量 a
、b
、ret
也不存在了
返回的值被賦值給咱們在第 9 步定義的變量 sum
最後,咱們在控制檯輸出了 sum
的值
如你所望,控制檯輸出的是 15。以上,咱們經歷了不少。有幾點我須要指出。首先,函數體的定義能夠存在一個變量中,直到程序調用了函數,函數的定義是不可見的。第二,每次函數被調用,一個新的本地上下文都會被建立(臨時的)。當函數執行完,隨之上下文也會消失。在遇到 return
語句或者閉合的大括號 }
就說明函數結束了。
看一下下面的代碼,並指出將會發生什麼。
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
複製代碼
如今,咱們已經從前面兩個示例中學到了竅門,咱們來按照以上的模式來逐步的分析下代碼。
第 1 - 8 行。咱們在全局上下文中建立了新的函數變量 createCounter
第 9 行。咱們在全局上下文中聲明瞭變量 increment
仍是第 9 行。咱們須要調用函數 createCounter
,並把結果賦值給你變量 increment
。
第 1 - 8 行。調用函數期間,會建立新的本地上下文。
第 2 行。在本地上下文中聲明瞭變量 counter
。默認值是數字 0
。
第 3 - 6 行。聲明瞭名爲 myFunction
的變量。這個變量是在本地上下文中聲明的。這個變量也是一個函數。第4 - 5 行就是相應的函數體。
第 7 行。直接放回了函數 myFunction
。本地上下文銷燬。myFunction
和 counter
也伴隨被銷燬。從新回調了調用上下文。
第 9 行。在調用上下文中,也就是全局上下文,createCounter
返回的值賦值給了變量 increment
。此時的變量就是一個函數。這個函數是由 createCounter
返回的。雖然,不是 myFunction
,可是,函數體內容是一致的。在全局上下文中,它就是 imcrement
。
第 10 行。聲明新變量(c1
)
第 10 行。調用了函數 increment
。這個函數是早期在第 4 - 5 行中定義的
建立新的上下文。只是執行函數,並無參數。
第 4 行。counter = counter + 1
。在本地上下文中查找變量 counter
。咱們只會建立上下文,絕對不會聲明任何本地變量。咱們看一下全局上下文。並無變量 counter
。所以,剛纔的表達式等同於 counter = undefined + 1
,聲明一個本地變量 counter
,並給它賦值數字 1
,由於,undefined
有點相似 0
。
第 5 行。咱們返回了 counter
的值,也就是數字 1
。同時,銷燬本地上下文和變量 counter
回到第 10 行。返回的值(1
)賦值給了 c1
第 11 行。重複第 10 - 14 步,c2
也獲得數字 1
第 12 行。重複第 10 - 14 步,c3
也獲得數字 1
第 13 行。咱們打印變量 c1
、c2
、c3
的值
本身試一下,看看會發生什麼。你會看到,並不會像我上面說的那樣輸出 1
、 1
和 1
。而是,輸出了 1
、2
、 3
。爲何?
莫名其妙,函數 increment
記住了 counter
的值。它是如何作到的?
難道 counter
是全局上下文的一部分?試着在控制檯打印 console.log(counter)
,你會看到輸出 undefined
。這說明它並不在全局上下文中。
或許,當你調用 increment
時,做用域回到了函數被建立的地方(createCounter
)?怎麼會呢?變量 increment
只是有着相同的函數體,並非 createCounter
。所以,也不對。
所以,必然有另一種機制。就是閉包。咱們最終說到它了。
如下就是它的工做模式。每當你聲明一個新函數,並把它賦值給一個變量,用來存儲函數的定義,這就是閉包。閉包包含建立函數時做用域內的全部變量。這就相似一個揹包。函數定義時附帶一個小揹包。這個揹包存儲了建立函數時做用域中全部的變量。
所以,以上的分析是錯誤的,咱們從新正確的分析一次。
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
複製代碼
第 1 - 8 行。咱們在全局上下文中建立了新的函數變量 createCounter
。和上次同樣
第 9 行。咱們在全局上下文中聲明瞭變量 increment
。和上次同樣
仍是第 9 行。咱們須要調用函數 createCounter
,並把結果賦值給你變量 increment
。和上次同樣
第 1 - 8 行。調用函數期間,會建立新的本地上下文。和上次同樣
第 2 行。在本地上下文中聲明瞭變量 counter
。默認值是數字 0
。和上次同樣
第 3 - 6 行。聲明瞭名爲 myFunction
的變量。這個變量是在本地上下文中聲明的。這個變量也是一個函數。第4 - 5 行就是相應的函數體。如今,我建立了一個閉包,它是函數的一部分。閉包包含當前做用域的中的變量,在這個示例中變量是 counter
(它的值是0
)。
第 7 行。直接放回了函數 myFunction
。本地上下文銷燬。myFunction
和 counter
也伴隨被銷燬。從新回調了調用上下文。所以,咱們獲得了一個函數和閉包,這個揹包中包含了函數定義時做用域中的全部變量。
第 9 行。在調用上下文中,也就是全局上下文,createCounter
返回的值賦值給了變量 increment
。此時的變量就是一個函數(也包括閉包)。這個函數是由 createCounter
返回的。雖然,不是 myFunction
,可是,函數體內容是一致的。在全局上下文中,它就是 imcrement
。
第 10 行。聲明新變量(c1
)
第 10 行。調用了函數 increment
。這個函數是早期在第 4 - 5 行中定義的。(而且變量也有一個揹包)
建立新的上下文。只是執行函數,並無參數。
第 4 行。counter = counter + 1
。咱們須要查詢變量 counter
。在此以前,我在本地或者全局上下文中查找,此次,咱們來看一下揹包,閉包。你瞧,閉包中包含一個名爲 counter
的變量,它的值是 0
。通過第 4 行的計算後,它的值變成 1
。它也從新存儲在揹包中。這時閉包中的變量 counter
值變成了 1
。
第 5 行。咱們返回了 counter
的值,也就是數字 1
。同時,銷燬本地上下文。
回到第 10 行。返回的值(1
)賦值給了 c1
第 11 行。重複第 10 - 14 步。此次,當咱們查看閉包時,看到變量 counter
的值是 1
。這是由於第 12 步致使的。這時,它的值再次被遞加獲得了 2
,並存儲在閉包中。同時,c2
的值也是 2
。
第 12 行。重複第 10 - 14 步,c3
也獲得數字 3
第 13 行。咱們打印變量 c1
、c2
、c3
的值
如今,咱們已經理解了它是如何工做的了。關鍵點在於,當函數被聲明時,它同時包含函數體和一個閉包。這個閉包會收集建立函數時做用域中的全部變量。
你或許會問,全部的函數都有閉包嗎?即便,是在全局做用域中聲明?是的。全局做用建立的函數也會建立閉包。可是,因爲函數是在全局做用域中被建立,所以,它們能夠訪問全局做用域中全部的變量。這並不徹底跟閉包有關。
當函數返回一個函數時,閉包的就比較重要了。返回的函數可訪問那些不在全局做用域,只存在於閉包中的變量。
有時,在你不經意間就會出現閉包。你或許在咱們應用中看到過相似的代碼。
let c = 4
const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
複製代碼
示例中的三角函數對你來講有點難以理解,它與下面的代碼效果同樣。
let c = 4
function addX(x) {
return function(n) {
return n + x
}
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
複製代碼
咱們聲明瞭一個普通的函數 addX
,它須要一個參數 x
,並返回了另一個函數。
這個返回的函數也須要一個參數,並用它與變量 x
相加。
變量 x
就是閉包的一部分。當變量 addThree
在本地上下文中聲明時,它獲得了一個函數和一個閉包。閉包中就有變量 x
。
所以,當調用和執行函數 addThree
時,它能夠經過閉包訪問變量 x
和作爲參數傳遞的 n
,最終返回了二者之和。
在這個示例中,控制檯將會輸出數字 7
。
爲了記住閉包,我把它比喻爲揹包。當一個函數被建立並傳遞或者經過另一個函數返回,它就會包含一個揹包。揹包中包含函數聲明時做用域中全部的變量。
若是,你喜歡這篇文章,不要吝嗇你的讚賞 👏
謝謝