什麼是閉包
在<<你不知道的JavaScript>>中解釋到: 當函式可以記住並訪問所在的詞法作用域時,就產生了閉包
變數作用域
因為閉包就是基於變數的作用域
變數的作用域
- 全域性變數
- 區域性變數
變數使用
區域性函式中使用全域性變數
var temp = 1;
function fn(){
console.log(temp)
}
fn()
複製程式碼
js可以在區域性函式中使用全域性作用域的變數 在內部函式fn中可以使用全域性變數temp
全域性作用域中使用函式中的區域性變數
function fn(){
var temp = 1
}
console.log(temp)
複製程式碼
- 執行結果為:Uncaught ReferenceError: temp is not defined at xxx.html 報錯.
- 所以在函式的外部是無法使用函式中的區域性變數
- 注意,在區域性函式中定義變數要使用var 關鍵字,否則,會定義為全域性變數
function fn(){
temp = 1
}
fn()
console.log(temp)
複製程式碼
這樣就可以列印temp的值了
使用區域性變數
如果想使用函式中的區域性變數,那麼就在函式的內部在定義一個函式,並返回
function f1(){
var temp = 'hello';
function f2(){
console.log(temp);
}
return f2
}
f1()()
複製程式碼
- f2定義在f1函式的內部,所以f2函式中可以使用f1內部的區域性變數(變數查詢是一層一層往上找的)
- 所以當執行f2函式的時候是可以列印f1中變數temp
閉包
- 首先有兩個函式,為父子關係
- 子函式引用父函式的變數
- 執行函式定義就會產生閉包(不需要呼叫內部函式)
演示閉包
閉包的生命週期
- 產生: 在巢狀內部函式定義執行完時就產生了閉包,而不是呼叫時(就是剛進入外部函式內部的時候就產生了閉包,因為函式提升,如果是變數接收方式定義內部函式的話,就是在執行該行程式碼時產生)
- 死亡: 在巢狀的內部函式成為垃圾物件時,xxx=null時
閉包的用途
- 可以使用函式內的區域性變數
- 這些變數可以始終保持在記憶體中
- 如果使用了單例模式,可以用到閉包
function f1() {
var temp = 100
function f2() {
console.log(temp)
}
add = function(){
temp += 1
}
return f2
}
var result = f1()
result()
add()
result()
result = null //釋放全域性變數,會銷燬常駐記憶體的區域性變數
複製程式碼
- 在函式f1額外定義了一個全域性回撥函式,並將temp+= 1
- 呼叫f2時,列印100
- 執行了add() temp+=1
- 在呼叫了f2,列印了101
- 如果一個物件不再被引用,那麼這個物件就會被垃圾回收機制回收
- 如果兩個物件互相引用,不與第三個物件產生關係,那麼這兩個物件也會被垃圾回收
- 當不使用的物件賦值null操作,釋放
這說明了,當f2被呼叫的時候,閉包並沒有被清除,變數仍然存在於記憶體中,給他進行+1操作,是在原變數的基礎上增加
- 因為閉包是父子巢狀關係,當子函式返回並用一個全域性變數接收,所以子函式會始終在記憶體中,而子函式又依賴於父函式,所以父函式也一直在記憶體中,所以不會在呼叫之後被垃圾回收機制回收
for迴圈問題
看一下下面程式碼的結果
function test(){
var arr = [];
for(var i = 0;i < 10;i++){
arr[i] = function(){
return i;
};
}
for(var a = 0;a < 10;a++){
console.log(arr[a]());
}
}
test();
複製程式碼
- 當列印的時候發現列印了10 個 10 ,這並不是我們想象的那種結果
但是換一種方式
function test(){
var arr = [];
for(let i = 0;i < 10;i++){
arr[i] = function(){
return i;
};
}
for(var a = 0;a < 10;a++){
console.log(arr[a]());
}
}
test();
複製程式碼
- 只是把for迴圈中的var 換成了let,結果就是我們想象的那樣了(0,1,2,...9)
這是為什麼
- 這個跟var 和let 的塊作用域有關,在es5中,for迴圈中的var並不代表著一個塊作用域,所以for迴圈中用var定義的是一個全域性變數,或者是for迴圈外函式的區域性變數,當迴圈結束,變數的值自然而然的變成了10
- 但是let就不一樣了,也會將for作為一個塊作用域(其實是離let最近的{},let可以繫結到任意塊作用域上),也就是說for迴圈內部與外部不在同一個作用域內
建議
- 正常函式的作用域或者其他變數的都會在函式執行之後被銷燬,但是建立了閉包會使區域性變數長期儲存在記憶體中,消耗記憶體很大,所以不能濫用,容易導致內訓洩露或效能問題,但是可以在退出函式時將不使用的區域性變數刪除(將 f1 = null... 處理)
- 閉包會在父函式外部,改變內部變數的值,所以存在操作失誤的可能
阮大神的兩個思考題
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
複製程式碼
解析
- 閉包中引用了this指標
- 因為閉包會賦值給全域性變數,所以那時this指向的其實是window物件
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
複製程式碼
解析
- 跟上面的區別就是將this賦值給了區域性變數,
- 因為閉包,所以只有在函式內部可以使用,當呼叫的的時候依然是函式內部的變數
一道經典的面試題
function fun(n,o){
console.log(o);
return {
fun: function(m){
return fun(m,n);
}
};
}
//n = 0 o為undefined
var a = fun(0);
// m = 1 n = 0 o為0
a.fun(1);
//n沒有變化 依然是0
a.fun(2);
//n沒有變化 依然是0
a.fun(3);
//undefined,0,1,2
//這是個難點:當fun(0) => n=0,o=undefined,當.fun(1)=> n=0,m=1,o=0,.fun(2)=> m=2,n=1,o=1,...
var b = fun(0).fun(1).fun(2).fun(3);
//undefined,0
var c = fun(0).fun(1);
//1
c.fun(2);
c.fun(3);
複製程式碼