尷尬了,遇到了一個閉包的問題,然後我說錯了答案,裝逼失敗了,之前我以為自己完全理解了閉包,現在發現其實並沒有,趕緊翻書找答案-ing。
看下面的程式碼,在迴圈中向陣列匯入函式, 希望可以列印 0,1,2
:
function func() {
var arr = [];
for(var i = 0;
i<
3;
i++){
arr.push(()=>
{
console.log(i);
})
} return arr
}var result = func();
result.forEach((item)=>
{
item();
})<
!-- 列印資訊 三個3 -->
// 3複製程式碼
發現列印的都是3,原因是匿名函式中的i
共享了同一個詞法作用域 。當變數陣列呼叫匿名函式時, var
宣告的變數不存在塊級作用域, i
的值已經指向了for
迴圈 i
的最後一項。
解決上面的問題
使用let
或者閉包
可以解決上面的問題,解決程式碼如下:
<
!-- 方案一 -->
// 使用let 宣告變數function func() {
var arr = [];
for(let i = 0;
i<
3;
i++){
arr.push(()=>
{
console.log(i);
})
} return arr
}var result = func();
result.forEach((item)=>
{
item();
})<
!-- 方案二 -->
// 使用閉包function func() {
var arr = [];
for(var i = 0;
i<
3;
i++){
(function(){
arr.push(()=>
{
console.log(i);
})
})()
} return arr
}var result = func();
result.forEach((item)=>
{
item();
})複製程式碼
以為已經解決了, 沒想出了其他問題!可以執行一下上面的方案一和方案二, 你會發現方案二的結果是列印出了三個3,WT?不是應該列印0、1、2
, 怎麼沒有?
閉包的作用域鏈
方案一中當然是沒有問題的,使用let
解決作用域問題。在方案二中, 使用閉包
解決變數i
的作用域問題,但是好像閉包
失效了。
在方案二中閉包的作用是變數私有化,儲存閉包作用域鏈,變數i
不被銷燬。對於閉包作用域不瞭解的可以 檢視冴羽的
JavaScript深入之執行上下文
和 JavaScript深入之閉包。而方案二中的結果卻不是我們想要的,究其原因,是我對閉包作用域
不理解導致的。
<
!-- 方案三 -->
function func() {
var arr = [];
for(var i = 0;
i<
3;
i++){
(function(i){
arr.push(()=>
{
console.log(i);
})
})(i)
} return arr
}var result = func();
result.forEach((item)=>
{
item();
})複製程式碼
以上就是解決方案,將i
加在匿名函式引數就解決了方案二的問題。
下面來細說一下是怎麼回事:
為了方便描述,我將 自執行的匿名函式 簡稱為 fn1, 而arr中的回撥匿名函式 簡稱為 fn2
。當然arr中的的三個函式分別是三個不同的 fn2函式。
函式作用域鏈
fn2函式作用域鏈 : {
fn2函式變數和引數 , fn1函式變數&
&
引數 , func函式變數&
&
引數 , 全域性作用域變數
}複製程式碼
當呼叫func
函式, 函式fn1
自執行,變數arr
被注入三個fn2
函式,同時arr
被return
出來。此時形成了閉包作用域鏈
。
陣列中的fn2
函式執行向上不斷查詢i,fn1
函式和fn2
函式中都不存在i
,直到找到func
函式變數i,此時由於i
是var
宣告的,不存在塊級作用域,三個fn2
函式共享i
。(其實這個地方有點和最開頭的解釋重複了,不過這裡出現的新東西可能就是閉包作用域鏈
了)。
在正常函式呼叫後,作用域鏈
被銷燬,但當存在閉包
時,對應的作用域鏈
會被儲存。 arr
中的fn2
函式作用域,就基本形成一個作用域鏈
,作用域鏈是單向的,內部向外部查詢,由下向上查詢
。作用域鏈中會儲存區域性變數、全域性變數、函式引數
。
比較方案二 與 方案三
// 方案二function func() {
var arr = [];
for(var i = 0;
i<
3;
i++){
(function(){
arr.push(()=>
{
console.log(i);
})
})()
} return arr
}var result = func();
result.forEach((item)=>
{
item();
})複製程式碼
現在再來看
方案二: 遍歷result呼叫時,arr
中的fn2
函式會先查詢自身函式作用域
,不存在i那麼就向上繼續查詢,找到func
函式下的變數i
,將i
列印出來,但此時func
函式中的變數i
已經等於3了, 由於for
迴圈三次,arr
中有三個fn2函式
, 同上, 所以列印了三次3。
// 方案三function func() {
var arr = [];
for(var i = 0;
i<
3;
i++){
(function(i){
arr.push(()=>
{
console.log(i);
})
})(i)
} return arr
}var result = func();
result.forEach((item)=>
{
item();
})複製程式碼
而在
方案三: 遍歷result呼叫時, arr
中的fn2
函式會先查詢自身函式作用域
,不存在i
那麼就向上繼續查詢,找到fn1
函式中引數i
, 將i列印出來。 由於存在三個匿名函式,所以函式引數分別是0、1、2
。
結尾: 其實說到這裡,基本可以瞭解,方案二中的問題,其實就是閉包作用域鏈
的問題,當形成閉包時,閉包涉及到的作用域鏈
會被儲存。如果真正的瞭解了閉包,絕對不會遇見像我這樣的問題,算是給我自己上了一課。寫的不好的地方希望大家可以指出,下面是參考的文章連結。
(如果上面的內容引起你的不適感,可以參考 冴羽的blog )
參考文章: