本文主要由淺入深,研究幾個問題
問題1:閉包是如何形成的?
1、都知道,閉包的典型特征就是函數里面返回函數,注意閉包不是一種函數,而是一種現象。它的作用是可以讓內部函數訪問到外部函數的變量,而外部無法訪問內部的變量。
function foo(){
var a = 1;
function fn(){
a++;
console.log('inner a',a);
}
console.log('outer a',a)
return fn;
}
foo(); //輸出outer a,1
foo()(); //輸出inner a,2和outer a:1
2、分析下這段代碼,內部函數fn需要使用變量a,當一個函數被執行時,會創建一個執行上下文(代碼的執行環境),而a這個變量的聲明並不在fn的執行環境中,所以只能在fn的詞法作用域中去尋找a,此時我們可以理解為fn的詞法作用域就是foo的作用域。
所以這邊存在了一個現象:變量a是在foo的執行上下文中被創建的,但是fn卻包裹着被改變量的引用,只要變量存在着引用,那么這個變量是無法被釋放的,這就形成了閉包。
3、順着上面的代碼,再度執行foo()和foo()();
function foo(){
var a = 1;
function fn(){
a++;
console.log('inner a',a);
}
console.log('outer a',a)
return fn;
}
foo(); //輸出outer a,1
foo()(); //輸出inner a,2和outer a:1
foo(); //輸出outer a,1
foo()(); //輸出outer a,1
我們發現,打印出來的結果跟上次的完全一樣。這很好解釋,每次函數執行時,會創建一個執行上下文,這個執行上下文會被推到執行棧中,並且瀏覽器會給每個變量分配內存。執行完畢以后,內存回收,執行上下文出棧,下次執行繼續重復上面的的過程。
變量a正常被GC回收。
4、什么時候變量a無法被回收呢?將代碼稍微調整下,此處不研究outer a了,故將其去掉。
function foo(){
var a = 1;
function fn(){
a++;
console.log('inner a',a);
}
return fn;
}
var f = foo();
f(); //inner a,2
f(); //inner a,3
f(); //inner a,4
發現每執行一次f,變量a就會+1,仿佛被記憶了一樣,這又是為何呢?跟上面的區別主要是在於一個是直接執行,一個是將函數賦值個一個變量來執行。為什么結果不同?
此處我給的解釋是:var f = foo(); 這邊的 f 實際上是一個全局變量,而在js變量回收規則里面,全局變量是不會被回收的。同時var f = foo()實際上創建了一個內部函數fn的引用,因此它可以訪問到fn執行時所在的代碼環境,而a一直存在於這個
環境里,就有了不被回收的理由。
問題2:來看看hooks里面過時的閉包
function create(i){
let value = 0;
return function(){
value += i;
console.log('value',value)
const message = `The current value is ${value}`;
return function logValue(){
console.log(message);
}
}
};
var inc = create(1);
var log = inc(); // value 1
inc(); // value 2
inc(); // value 3
log(); // The curent value is 1
可以看到異常了,inc執行了三次以后,value應該是3,但是最后執行log的時候打印出來的message顯示的value卻是1,此處成log為過時的閉包,message為過時的變量。OK,那value到底是多少呢?繼續在logValue里面將value打印出來。
function create(i){
let value = 0;
return function(){
value += i;
console.log('value',value)
const message = `The current value is ${value}`;
return function logValue(){
console.log(value,message);
}
}
};
var inc = create(1);
var log = inc(); // value 1
inc(); // value 2
inc(); // value 3
log(); // 3 The curent value is 1
正應了上面所述,value並沒有過時,過時的變量是message。
為什么message是過時的變量呢?對這段代碼進行分析:
1)var inc = create(1);創建了一個變量inc,該變量返回的是create函數里面第一層的匿名函數的引用。匿名函數被執行時,創建了執行上下文,而value一直存在與這個執行上下文中。但message也在啊,為什么會過期呢?
2)value跟message雖然都存在於同一個執行上下文里面,但是它們的生命周期不一樣,value不是在當前執行上下文被創建的,而是在其詞法作用域即外部的create()函數里面被創建的,根據閉包形成的條件,變量value不被釋放。
而message就不同了,每次調用這個匿名函數的時候,message都會被執行上下文重新創建,重新分配內存。
3)var log = inc();創建了一個變量log,該變量返回的是執行了一次inc以后的logValue函數的引用,此時value=1.后面inc()被調用了多次,value發生了變化.
4) 最后執行log()的時候,即執行了logValue(),此時的message不在當前執行上下文中,要去其詞法作用域中尋找,而message所在的詞法作用域即第一層匿名函數,早先賦值給了log,所以輸出了一個過時的結果。
問題3:如何解決過時的閉包問題?
原理理解了就好辦了,因為我們發現value並沒有過時,所以調整下message的位置即可。
function create(i){
let value = 0;
return function(){
value += i;
console.log('value',value)
return function logValue(){
const message = `The current value is ${value}`;
console.log(value,message);
}
}
};
var inc = create(1);
var log = inc(); // value 1
inc(); // value 2
inc(); // value 3
log(); // 3 The curent value is 3
或者改變下var log = inc()的順序,使用新的閉包。此處略。
