從一道簡單面試題來解讀JS中的閉包和作用域
先上程式碼
var count=10;//全域性作用域 標記為f1
function add(){
var count=0;//函式全域性作用域 標記為f2
return function(){
count+=1;//函式的內部作用域
alert(count);
}
}
var s=add()
s();//輸出1
s();//輸出2
加()的返回值是一個函式,首先第一次呼叫()的的時候,是執行加()的返回的函式,也就是下面這個函式:
function(){ count+=1;//函式的內部作用域 alert(count); }
也就是將計數+ 1,再輸出,那計數是從哪兒來的的呢,根據作用域鏈的規則,底層作用域沒有宣告的變數,會向上一級找,找到就返回,沒找到就一直找,直到視窗的變數,沒有的話就返回undefined。這裡明顯計數是函式內部的f2的那個計數。
如果把F2的計數刪掉,我們再來看
var count=10;//全域性作用域 function add(){ //var count=0;註釋掉了 return function(){ count+=1;//函式的內部作用域 alert(count); } } var s=add() s();//輸出11 s();//輸出12
自然這是體現不出閉包的性質,只為了說明函式作用域鏈。
我們再來看原來的程式碼,第一次執行,是沒有疑問的輸出1,那第二次的過程是怎樣的呢?繼續執行那個函式的返回的方法嗎,還是執行返回方法中的計數+ = 1;然後再輸出計數?這裡問題就來了,不應該繼續向上尋找,找到計數= 0;然後還輸出1嗎?不知道你有沒有注意一個問題,那就是s()執行的是下面這個函式
function(){
count+=1;//函式的內部作用域
alert(count);
}
也就是說加(),只被執行了一次。然後執行兩次S(),那計數的值就是隻聲明瞭一次。
var s = add(),函式add only在這裡執行了一次。
下面執行的都是S(),那第二次的計數的值是從哪兒來的,沒錯它還是第一次執行新增時,留下來的那個變數。
但是函式變數執行完就會被釋放啊,為什麼還在?這裡就是一個垃圾回收機制的引用計數問題。
如果一個變數的引用不為0,那麼他不會被垃圾回收機制回收,引用,就是被呼叫。
由於再次執行s()的時候,再次引用了第一次add()產生的變數計數,所以計數沒有被釋放,第一次s(),count的值為1,第二次執行s(),計數的值再加1,自然就是2了。
讓我們返回來再看看,根據以上所說,如果執行兩次add(),那就應該輸出都是1,來改一下這個函式
function add(){
var count=0;//函式全域性作用域
return function(){
count+=1;//函式的內部作用域
alert(count);
}
}
add()();//輸出1
add()();//輸出1
如之前所想的一樣,輸出的兩次都是1。
另外,用一句話來解釋閉包,“閉包就是能夠讀取其他函式內部變數的函式”。
說道閉包,就必然會扯出函式的作用域
ES5變數的作用域無非就是兩種:全域性變數和區域性變數,JS語言的特殊之處,就在於函式內部可以直接讀取全域性變數。
var n=1;
function f1(){
alert(n);
}
f1(); // 1
function f1(){
var n=1;
}
alert(n); // error
這裡有一個地方需要注意,函式內部宣告變數的時候,一定要使用VAR命令。如果不用的話,你實際上聲明瞭一個全域性變數。
function f1(){
n=1;
}
f1();
alert(n); // 1
有時候需要得到函式內的區域性變數,在函式的內部,再定義一個函式。
function f1(){
n=1;
function f2(){
alert(n); // 1
}
}
在上面的程式碼中,函式f2就被包括在函式f1內部,這時f1內部的所有區域性變數,對f2都是可見的。但是反過來就不行,f2內部的區域性變數,對f1就是不可見的。這就是Java指令碼語言特有的“鏈式作用域”結構,子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。
既然F2可以讀取F1中的區域性變數,那麼只要把F2作為返回值就可以在F1外部讀取它的內部變量了。
function f1(){
n=1;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 1
使用閉包的注意點
1)由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。
2)閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(物件)使用,把閉包當作它的公用方法,把內部變數當作它的私有屬性,這時一定要小心,不要隨便改變父函式內部變數的值。
閉包的用途
閉包可以用在許多地方。它的最大用處有兩個,的英文一個前面提到的可以讀取函式內部的變數,就是另一個讓這些變數的值始終保持在記憶體中。
怎麼來理解這句話呢?請看下面的程式碼。
function f1(){
var n=1;
Add=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 1
Add();
result(); // 2
在這段程式碼中,導致實際上就是閉包F2函式。它一共運行了兩次,第一次的值是1,第二次的值是2,這證明了,函式F1中的區域性變數Ñ一直儲存在記憶體中,並沒有在F1呼叫後被自動清除。
為什麼會這樣呢?原因就在於F1是F2的父函式,而F2被賦給了一個全域性變數,這導致F2始終在記憶體中,而F2的存在依賴於F1,因此F1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制回收。
這段程式碼中另一個值得注意的地方,就是“Add = function(){n + = 1}”這一行,首先在Add前面沒有使用var關鍵字,因此新增是一個全域性變數,而不是區域性變數。其次,新增的值是一個匿名函式,而這個匿名函式本身也是一個閉包,所以新增相當於是一個二傳手,可以在函式外部對函式內部的區域性變數進行操作。