正如《你不知道的 JavaScript》書中所言:javascript
閉包就好像從 JavaScript 中分離出來的一個充滿神祕色彩的未開化世界,只有最勇敢的人才能到達那裏!前端
在實際的開發工做中也確實如此,除了在面試的場景下,或其它幾個少數特定的場景下(如「防抖節流」函數),咱意識到了 —— 這就是「閉包」!其它時候基本不用,或者是用了殊不知道。java
多麼惋惜!!這麼好的東西用了卻不自知。web
本篇藉助 medium 上的五萬贊好文 I never understood JavaScript closures 帶你一次吃透「閉包」!(吃不透找我,找耶穌也沒用,我說的 QAQ)面試
看完本篇,你會驚奇的發現,居然連如下這段代碼都存在着閉包?!瀏覽器
let a = 1
function b(){
return a
}
console.log(b())
複製代碼
「我永遠不懂 JS 閉包,由於它無處不在......」markdown
認知「閉包」前,咱得了解一個重要的概念 —— 「執行上下文」!閉包
執行上下文分爲:app
好比咱們在全局執行上下文中調用一個函數的時候,JS 解析的流程大概會是這樣的:ide
return
或 }
時,斷定爲執行結束。關於執行棧的操做,可見下面的動圖理解:
咱們再結合下面這段代碼舉例解析,來看看 JS 一步一步究竟是怎麼作的:
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)
複製代碼
第一行,在全局執行上下文聲明瞭一個變量 a,賦值爲 3;
第二行到第五行是函數執行上下文。是在全局執行上下文聲明瞭一個 addTwo 的函數,函數內部的代碼不作執行,只是存儲着以供後面調用;
第六行,聲明瞭一個變量 b,賦值 b 爲 addTwo 函數執行的返回值;
在全局執行上下文找到 addTwo 函數進行執行,並傳入參數 3 ;
此時,執行上下文會進行切換!建立一個臨時的名爲 addTwo 的函數執行上下文,推到執行棧當中;
到第三行,而後是怎樣?建立一個變量 ret 嗎?不對,實際上,是先建立一個變量 x 而後賦值爲 3 ;
第三行,聲明一個變量 ret ,而後賦值爲 x + 2 的運算結果。
JS 會找加法的項 x 在哪?原來 x 在 addTwo 這個函數執行上下文就已經有了,它的值是 3 ,接着與 2 執行加法運算,而後賦給 ret。
而後來到了第四行,將 ret 進行 return 返回;
第4、第五行,addTwo 函數執行結束,臨時的執行上下文被銷燬;變量 x 和變量 ret 都會被清除;函數執行上下文將被在調用棧被推出,而後把函數返回給調用它的執行上下文,這裏也就是全局執行上下文;
而後回到第 4 點,將函數返回值賦值給 b;
第七行,進行打印;
這裏雖然是一段很是冗長的解釋,且還並未涉及到「閉包」,可是倒是咱們必需要寫在前面而且理解透徹的。
接着「執行上下文」,還有一個概念咱們須要理解,它就是 「詞法做用域」!
來看下面這段代碼示例:
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)
複製代碼
你能參照上一節的解釋對這段代碼進行描述嗎?
第一行,在全局執行上下文聲明瞭一個變量 vall,賦值爲 2;
第二行至第五行聲明一個 multiplyThis 函數執行上下文,內部代碼不作執行,存儲以供調用;
第六行,聲明一個變量 multiplied,賦值爲 multiplyThis 函數執行的返回值;
在全局執行上下文找到 multiplyThis 函數進行執行,並傳入參數 6 ;
此時,執行上下文會進行切換!建立一個臨時的名爲 multiplyThis 的函數執行上下文,推到執行棧當中;
到第三行,在函數執行上下文聲明一個變量 n ,並賦值爲 6;
第三行,聲明一個變量 ret ,而後賦值爲 n 與 vall 作乘法的運算結果。n 爲 6,vall 呢?在當前函數執行上下文並未找到 vall!
此時,JS 會去調用 multiplyThis 函數的全局執行上下文尋找 vall!找到了!它的值是 2,6 * 2 = 12。賦給 ret;
而後來到了第四行,將 ret 進行 return 返回;
第4、第五行,multiplyThis 函數執行結束,臨時的執行上下文被銷燬,變量 n 和變量 ret 都會被清除,可是 vall 沒有被銷燬,由於它存在於全局函數執行上下文;
回到第六行,將返回值 12 賦給變量 multiplied;
最後打印輸出;
這段描述中,置灰的步驟就是和上一節的描述基本一致,未置灰的是
最重要的是:JS 在當前執行上下文尋找變量的時候,若是找不到,就會去調用這個執行上下文的上一執行上下文去尋找。
這並不難理解,這樣鏈式查找變量的過程,就是 JS 的【做用域鏈】。
函數能夠返回任何東西,固然也就包括返回另外一個函數了。
讓咱們再按照以上解析一遍如下代碼:
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)
複製代碼
第一行,在全局執行上下文聲明瞭一個變量 val,賦值爲 7;
第二行至第八行聲明一個 createAdder 函數執行上下文,內部代碼不作執行,存儲以供調用;
第十一行,聲明一個變量 adder,賦值爲 createAdder 函數執行的返回值;
在全局執行上下文找到 createAdder 函數進行執行,它在第二行,ok,調用;
來到第二行,建立一個新的臨時的函數執行上下文,將其推倒執行棧;
因爲 createAdder 函數沒有傳參,直接進入函數體內,這裏聲明瞭一個函數 addNumbers,只作聲明,不作執行;
來到第七行,將 addNumbers 返回到全局執行上下文,它是一個函數;而後將臨時 createAdder 函數執行的上下文推出執行棧;
createAdder 函數執行上下文被銷燬,此時 addNumbers 也將不存在;
再到第十行,在全局執行上下文聲明一個變量 sum,它的值爲 adder(val, 8) 執行的返回值;
咱們再到全局執行上下文尋找 adder ,找到了,它正好是一個函數,咱們能夠調用它;
他有兩個傳參,第一個是 val,它在第一行聲明瞭並複製了,爲 7,第二個參數是 8;
而後咱們來到第三行到第五行,建立兩個變量 a 和 b ,爲他們賦值分別爲 8 和 7;
第四行,聲明一個變量 ret ,賦值爲 8 + 7,爲 15;
ret 變量被 return ,臨時的函數執行上下文被銷燬,a ,b ,ret 變量都將被銷燬;
adder 函數執行的返回值賦給 sum 變量;
最後打印輸出;
看了以上三段的具體步驟詳細分析,我相信再給你一段相似的調用代碼,你也必定能通曉其中端倪,做出相似的解析!
1:let counter
2: function createCounter() {
3: let counter = 0
4: const myFunction = function() {
5: counter = counter + 1
6: return counter
7: }
8: return myFunction
9: }
10: const increment = createCounter()
11: const c1 = increment()
12: const c2 = increment()
13: const c3 = increment()
14: console.log('example increment', c1, c2, c3)
複製代碼
來試試吧?
第一行,聲明一個變量 counter ,賦值爲 undefined ;
第二行至第九行,在全局執行上下文聲明瞭一個函數 createCounter ,不作執行,存儲以供調用;
第十行,在全局執行上下文聲明瞭一個變量 increment ,賦值爲 createCounter 函數執行的返回值;
調用 createCounter 函數,它在第 2 步已經聲明瞭,執行它!
第二到九行,建立一個新的臨時的函數執行上下文;
在這個臨時的函數執行上下文聲明一個變量 counter,賦值爲 0;
第四到七行,聲明一個 myFunction 函數,不作執行,存儲以供調用;
將 myFunction 進行返回,賦給變量 increment。createCounter 函數執行上下文被銷燬,myFunction 和 counter 都將被銷燬;
此時的全局執行上下文沒有 myFunction 函數了,只有 increment 函數;
第十一行,聲明一個變量 c1 , 賦值爲 increment 函數執行的返回值;
increment 函數是 createCounter 函數執行的返回值,它在第四行到第七行被定義;
建立一個新的函數執行上下文,沒有傳參,直接進入函數內部進行執行;
第五行,counter = counter + 1
,counter 變量在新建立的函數執行上下文找的到嗎?找不到!只得回到調用它的全局執行上下文去尋找,在全局執行上下文 counter 爲 undefined,因此執行 counter = undefined + 1
;
第六行,返回 counter,值爲 1,銷燬新函數執行上席文;
回到第十一行,c1 賦值爲 1;
第十二行,重複步驟第 10 到第 14 步,c2 同理賦值爲 1;
第十三行,重複步驟第 10 到第 14 步,c3 同理賦值爲 1;
最終打印輸出;
到底是這樣的嗎? 咱們在控制檯打印:
結果居然與咱們分析的指望打印相悖!
發生了什麼?
必定還有一個神祕的東西在做用!沒錯,它就是 「閉包」!the missing piece !
它的原理是這樣的:
當咱們聲明一個函數時,存儲以供調用,存儲的不只僅是這個函數的定義,同時還有這個函數的「閉包」,閉包包括了這個函數執行上下文全部變量的詞法做用域。這些在函數被建立時就已經肯定下來了。
作個不太恰當的比喻,把函數理解爲一我的,當這人生下來的時候(函數建立時),也附贈了一個揹包(閉包),這個揹包包括了家庭環境(詞法做用域)。
因此咱們上一段的逐步分析是錯的!
咱們再來進行一次正確的分析!
1:let counter
2: function createCounter() {
3: let counter = 0
4: const myFunction = function() {
5: counter = counter + 1
6: return counter
7: }
8: return myFunction
9: }
10: const increment = createCounter()
11: const c1 = increment()
12: const c2 = increment()
13: const c3 = increment()
14: console.log('example increment', c1, c2, c3)
複製代碼
第一行,聲明一個變量 counter ,賦值爲 undefined ;(同上)
第二行至第九行,在全局執行上下文聲明瞭一個函數 createCounter ,不作執行,存儲以供調用;(同上)
第十行,在全局執行上下文聲明瞭一個變量 increment ,賦值爲 createCounter 函數執行的返回值;(同上)
調用 createCounter 函數,它在第 2 步已經聲明瞭,執行它!(同上)
第二到九行,建立一個新的臨時的函數執行上下文;(同上)
在這個臨時的函數執行上下文聲明一個變量 counter,賦值爲 0;(同上)
第四到七行,聲明一個 myFunction 函數,同時還會建立一個閉包,包括這個函數執行上下文全部變量的詞法做用域。此例中就是 counter,經過做用域鏈查找,它的值爲 0。這裏一樣不作執行,存儲以供調用;
將 myFunction 和它的閉包 一塊兒進行返回,賦值給 變量 increment。createCounter 函數執行上下文被銷燬,myFunction 和 counter 都將被銷燬;
此時的全局執行上下文沒有 myFunction 函數了,只有 increment 函數;(同上)
第十一行,聲明一個變量 c1 , 賦值爲 increment 函數執行的返回值;(同上)
increment 函數是 createCounter 函數執行的返回值,它在第四行到第七行被定義;(同上)
建立一個新的函數執行上下文,沒有傳參,直接進入函數內部進行執行;(同上)
第五行,counter = counter + 1
,counter 變量在新建立的函數執行上下文找的到嗎?找不到!咱們要去它的「閉包」裏面找一找!,居然是有的!它的值爲 0 ,那麼就能夠執行 counter = 0 + 1,等於 1;
第六行,返回 counter,值爲 1,銷燬新函數執行上席文;(同上)
回到第十一行,c1 賦值爲 1;(同上)
第十二行,重複步驟第 10 到第 14 步,咱們再去「閉包」裏找的時候,由於通過上一步的操做 counter 已經變爲 1 了,因此再執行加法運算,counter 結果爲 2 ;
第十三行,重複步驟第 10 到第 14 步,c3 同理賦值爲 3;
最終打印輸出;
噢~原來是這樣!
當一個函數聲明的時候不僅僅只作了聲明,後面還帶着個「閉包」呢!閉包裏裝的是這個函數執行上下文全部變量的詞法做用域!
原來這就是閉包!
你可能會疑問:只要一個函數進行了聲明,就包含了閉包,那全局函數是否是也有閉包呢?
答案是:YES !
全局函數聲明時,也有閉包!只不過它的變量詞法做用域就是全局的,因此不是「閉」的不是很明顯。
當一個函數返回另一個函數時,「閉包」是最明顯的!返回的函數的變量詞法做用域能夠訪問非全局範圍的變量,它們僅放在其閉包中!
這裏再放幾道閉包題目,供你們自查測驗:
// 題目一
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)
// 題目二
function multiply(number1, number2) {
if (number2 !== undefined) {
return number1 * number2;
}
return function doMultiply(number2) {
return number1 * number2;
};
}
multiply(4, 5);
const double = multiply(2);
double(5);
// 題目三
function showBiBao() {
for (var i = 0; i < 5; i++) {
setTimeout( timer => () {
console.log(i);
}, 1000 );
}
console.log(i)
}
showBiBao()
function showListNumber() {
for(var i = 0; i < 5; i++) {
let ret = function(i) {
setTimeout(function timerr() {
console.log(i)
}, 1000)
}
ret(i)
}
console.log(i)
}
showListNumber()
複製代碼
我相信若是你從頭到尾認真跟着分析步驟來了,這必定不算啥大問題~
歡迎討論 ~
經過本篇咱們知道了:只要有函數申明的地方就有閉包!只不過有時候「閉」的不那麼明顯。
爲何題目說《我永遠不懂 JS 閉包》呢?其實你也看到了,開發工做中,即便你沒有用到閉包或者根本不認識閉包也同樣摸魚打卡上下班。但問題的關鍵是閉包嗎?
不!
你覺得本篇是在講閉包?錯!本篇是在講執行上下文!
你覺得本篇是在講執行上下文?錯!本篇是在講詞法做用域!
你覺得本篇是在講詞法做用域?錯!本篇是在講 JS 的動態語言特性!
你一問本篇是在講 JS 的動態語言?錯!本篇是在講 JS 運行的單線程特性!
......
沒錯,哎,就是玩兒~
撰文不易✍,點贊鼓勵👍,你的反饋💬,個人動力🚀
我是掘金安東尼,關注公衆號【掘金安東尼】,關注前端技術,也關注生活,持續輸出ing!