你關注了我,是個概率極低的事件......
閱讀本文大概需要二十分鐘
一直有一些剛入門js的朋友問我“什麼是閉包?”,這裡我就專門總結一下,下次再有人問起來,就直接把這篇文章給他看好了。
為什麼閉包這麼重要?
這跟js語言的特性有著密切的聯絡。要想徹底理解閉包,需要首先理解js的作用域概念
(與類C語言的作用域不同!)、立即執行函式
(IIFE)。如果不瞭解這兩個知識點,可以先看文字下半部分對著兩個知識點的介紹。
一. 閉包?
其實很簡單啊。
閉包就是一個函式(外部函式)內部又定義了一個函式(內部函式),內部函式可以訪問外部函式中宣告的所有變數。
官方解釋就比較晦澀了: 閉包是一個擁有許多變數和綁定了這些變數環境的表示式。
閉包的核心是什麼? 由於作用域的關係,我們在函式外部是無法直接訪問到函式內部的變數的,閉包就是有權訪問另一個函式內部作用域的變數的函式
如何從記憶體角度理解閉包?
- JavaScript具有自動垃圾回收機制,函式執行完之後,其內部變數就會被銷燬;
- 閉包就是在外部可以訪問此函式作用域的變數,所以閉包的一個特點就是只要存在引用函式內部變數的可能,JavaScript就需要在記憶體中保留這些變數,而且JavaScript執行時需要跟蹤這個內部變數的所有外部引用,直到最後一個引用被解除(置為null或者頁面關閉),JavaScript垃圾收集器才釋放相應的記憶體空間。
舉個例子,
<script type="text/javascript">
function outer(){
var a = 1 ;
function inner(){
return a++;
}
return inner;
}
var abc = outer();
//outer()只要執行過,就有了引用函式內部變數的可能,然後就會被儲存在記憶體中;
//outer()如果沒有執行過,由於作用域的關係,看不到內部作用域,更不會被儲存在記憶體中了;
console.log(abc());//1
console.log(abc());//2
//因為a已經在記憶體中了,所以再次執行abc()的時候,是在第一次的基礎上累加的
var def = outer();
console.log(def());//1
console.log(def());//2
//再次把outer()函式賦給一個新的變數def,相當於綁定了一個新的outer例項;
//console.log(a);//ReferenceError: a is not defined
//console.log(inner);//ReferenceError: a is not defined
//由於作用域的關係我們在外部還是無法直接訪問內部作用域的變數名和函式名
abc = null;
//由於閉包占用記憶體空間,所以要謹慎使用閉包。儘量在使用完閉包後,及時解除引用,釋放記憶體;
</script>
二. JavaScript的作用域?
作用域控制著變數與引數的可見性與生命週期
,包括三個概念:函式作用域
、塊級作用域
、作用域鏈
。
- 函式作用域
這個很好理解,函式作用域就是說定義在函式中的引數和變數都是函式外部不可見的。這裡不做額外介紹,無非就是函式中定義的變數在函式外是無法訪問的。
需要注意的是,函式內宣告的所有變數在函式體內始終可見的,這也就是變數宣告提前
。
var scope="global";
function scopeTest(){
console.log(scope);
var scope="local"
}
scopeTest(); //undefined
此處輸出是undefined
,並沒有報錯,這是因為函式體內宣告的變數都在函式體內始終可見。上面程式碼等效於:
var scope="global";
function scopeTest(){
var scope;
console.log(scope);
scope="local"
}
scopeTest(); //local
- 塊級作用域
任何一對花括號中的語句都屬於一個塊,在這之中定義的變數在括號外無法訪問,這叫做塊級作用域。
大多數類C語言都有塊級作用域的,而JavaScript並不支援塊級作用域,它只支援函式作用域,而且一個函式中任何位置定義的變數在該函式中的任何位置都是可見的。
function scopeTest() {
var scope = {};
if (scope instanceof Object) {
var j = 1;
for (var i = 0; i < 10; i++) {
//console.log(i);
}
console.log(i); //輸出10
}
console.log(j);//輸出1
}
在JavaScript中變數的作用範圍是函式級的,所以會在for迴圈後輸出10,在if語句後輸出1。又比如:
var scope = "hello";
function scopeTest() {
console.log(scope);//①
var scope = "no";
console.log(scope);//②
}
在①處竟然輸出undefined
,因為上述程式碼等效於:
var scope = "hello";
function scopeTest() {
var scope;
console.log(scope);//①
scope = "no";
console.log(scope);//②
}
那麼在Javascript中如何模擬塊級作用域呢?
可以使用IIFE
來模擬一個塊級作用域:
(function (){
//內容
})();
舉例,
function test(){
(function (){
for(var i=0;i<4;i++){
}
})();
alert(i);
}
test();
函式執行完,彈出的是i
未定義的錯誤。
- 作用域鏈 JavaScript中每個函式都有自己的執行上下文環境,當函式呼叫時會建立變數物件的作用域鏈,作用域鏈是一個物件連結串列,它保證了變數物件的有序訪問。 變數的查詢會從當前作用域開始找,如果沒有就繼續向上級作用域鏈查詢,直到找到全域性物件中。
三. 立即執行函式?
前面講到立即執行函式,很多人會把它和閉包混為一談,這裡做一下區分。
它們有著不同的作用,立即執行函式模擬的是塊級作用域,防止變數全域性汙染
,而閉包有不同的作用。
立即執行函式是指宣告完便立即執行的函式,這裡函式通常是一次性使用的,因此沒必要給函式命名,直接讓它執行就好了。
所以,立即執行函式的形式應該如下:
<script type="text/javascript">
function (){}(); // SyntaxError: Unexpected token (
//引擎在遇到關鍵字function時,會預設將其當做是一個函式宣告,函式宣告必須有一個函式名,所以在執行到第一個左括號時就報語法錯誤了;
(function(){…})();
//在function前面加!、+、 -、=甚至是逗號等或者把函式用()包起來都可以將函式宣告轉換成函式表示式;我們一般用()把函式宣告包起來或者用 =
</script>
雖然立即執行函式是想在定義完函式後直接就呼叫,但是引擎在遇到關鍵字function時,會預設將其當做是一個函式宣告,函式宣告必須要有一個函式名,所以執行到第一個括號就報錯了。 正確地定義一個立即執行函式,是應該用括號把函式宣告包起來。 此外,實際應用中,立即執行函式還可用來寫外掛。
<script type="text/javascript">
var Person = (function(){
var _sayName = function(str){
str = str || 'shane';
return str;
}
var _sayAge = function(age){
age = age || 18;
return age;
}
return {
SayName : _sayName,
SayAge : _sayAge
}
})();
//通過外掛提供的API使用外掛
console.log(Person.SayName('lucy')); //lucy
console.log(Person.SayName());//shane
console.log(Person.SayAge());//18
</script>
四. 為什麼要用閉包?
- 符合函數語言程式設計規範 什麼是函數語言程式設計?它的思想是:把運算過程儘量寫成一系列巢狀的函式呼叫。舉例來說,要想程式碼中實現數學表示式:
(1 + 2) * 3 - 4
傳統的寫法是:
var a = 1 + 2;
var b = a * 3;
var c = b - 4;
函數語言程式設計要求儘量使用函式,把運算過程定義為不用的函式:
var result = subtract(multiply(add(1,2), 3), 4);
此外,函數語言程式設計把函式作為“一等公民”。函式與其他資料型別一樣,處於平等地位,可以賦值給其他變數,也可以作為引數傳入另一個函式,或者作為別的函式的返回值。
- 延長變數生命週期 區域性變數本來在函式執行完就被銷燬,然而閉包中不是這樣,區域性變數生命週期被延長。不過這也容易使這些資料無法及時銷燬,會佔用記憶體,容易造成記憶體洩漏。如:
function addHandle() {
var element = document.getElementById('myNode');
element.onclick = function() {
alert(element.id);
}
}
onclick儲存了一個element的引用,element將不會被回收。
function addHandle() {
var element = document.getElementById('myNode');
var id = element.id;
element.onclick = function() {
alert(id);
}
element = null;
}
此處將element設為null,即解除對其引用,垃圾回收器將回收其佔用記憶體。
五. tips:
- 如果閉包只有一個引數,這個引數可以省略,可以直接用it訪問該引數。
- 實際中閉包常常和立即執行函式結合使用。