在JS這塊,免不了被問什么是閉包。
從一個常見的循環問題說起。
有一個ul列表, 里面有5個li標簽,我希望點擊每個li標簽的時候,彈出每個li標簽對應的索引值(第一個彈出0,第二個彈出1...)。
<ul id="result"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul>
當我很認真的寫出一段代碼:
var lis = document.getElementsByTagName('li'), n = lis.length, i = 0; for(; i < n; i++){ lis[i].onclick = function(){ alert(i); } }
蠻高興的做了點擊測試,從第一個li標簽開始,彈出"5",第二個、第三個...全部都彈出“5”。什么情況,這不是我想要的結果。出現了問題,就去找問題的原因,當我點擊每個li標簽的時候,都彈出“5”,說明for循環已經運行完了,變量 i 也跟着循環條件增加到了5,在我點擊前,循環已經執行完成。
經過一番思考,重新寫了這段代碼:
var lis = document.getElementsByTagName('li'), n = lis.length, i = 0; for(; i < n; i++){ lis[i].index = i; lis[i].onclick = function(){ alert(this.index); } }
把每次循環時變量 i 的值賦值給每個li標簽對象的一個屬性。很神奇的這段代碼做到了我要的結果,點擊每個li標簽彈出了對應的索引值(第一個彈出0,第二個彈出1...)。這讓我想到是因為變量 i 沒有被正確的引用,才發生那都彈出5的問題。懵懵懂懂的想到要正確的引用 變量 i 。經過多次寫寫改改,點擊測試,寫出了下面這樣的代碼:
var lis = document.getElementsByTagName('li'), n = lis.length, i = 0; for(; i < n; i++){ (function(num){ lis[num].onclick = function(){ alert(num); } }(i)); } //或者 var lis = document.getElementsByTagName('li'), n = lis.length, i = 0; for(; i < n; i++){ lis[i].onclick = function(num){ return function(){ alert(num); } }(i); }
后來半知半解的明白了這是閉包的一種運用,閉包與變量的作用域、變量的生存周期有密切的關系。要理解閉包就要理解變量的作用域、變量的生存周期。好吧,得先了解與變量有關的知識了。
變量的作用域
當在一個函數中聲明一個變量的時候,如果我們沒有加上關鍵字 var, 這個變量就是全局變量,加上了關鍵字var,這個變量就是局部變量,只有在這個函數內部才能訪問這個局部變量,在函數外是訪問不到的。 函數的參數也是局部變量,只能在函數內部訪問。
function a(){ b = 1; //全局變量 var c = 2; //局部變量 } a(); alert(b); //1 alert(c); //出錯 c未定義
定義了一個函數a,里面有個全局變量b,局部變量c。當在函數a外部訪問變量b、c,正常彈出了b的值,而變量c是局部變量,沒有正常訪問,所以出錯,提示變量c未定義。
在函數內部,局部變量優先級高於同名的全局變量。當定義一個函數的時候,也會隨之創建一個函數作用域。當在函數內部訪問一個變量的時候,會在函數內部作用域搜索這個變量,如果函數內部沒有這個變量,會在函數外部搜索,直到找到這個名稱的變量為止。如果找不到,就會拋出一個錯誤。
var v = 1; function a(){ var m = 2; function b(){ var n = 3; alert ( m ); // 2 alert ( v ); // 1 } b(); alelrt ( n ); //出錯 } a();
上面的代碼第一次彈出變量m,首先在函數b里查找,但沒有找到,繼續往外找,在函數a里面找,m的值為2;第二次彈出變量v,同樣的函數b里找,沒有找到,再在函數a里找,也沒有找到,在往外找,找到了v的值為1;第三次彈出變量n,在函數a里面找,沒有找到,不能在函數b里面找,因為查找是向上、向外的,不能向下、向內,最后在函數a的外面找,也沒有找到,這時就拋出錯誤,變量n沒有定義。
上面的代碼有三個作用域,函數b的作用域,函數a的作用域,window全局作用域(最頂層的作用域),這些作用域聯合起來,就形成了作用域鏈。訪問變量就是在這個作用域鏈的一個搜索過程。
變量的生存周期
在javascript中,全局變量擁有很長的生存周期,直到把變量銷毀,而局部變量會隨着函數調用結束被銷毀。
function a(){ v = 1; //全局變量v alert(v); //1 } a(); alert(v) //1 全局變量v還存在 function b(){ var i = 2; //局部變量i alert(i); //2 } b(); alert(i) //出錯 i未定義 局部變量i已經被銷毀
上面的代碼定義了兩個函數,函數a內部定義全局變量v,函數a執行完后全局變量v還存在;函數b內部定義了局部變量i,函數b執行完后局部變量i被銷毀。
function c(){ var k = 3; //局部變量k return function(){ k++; alert(k); } } var f = c(); f(); //4 f(); //5 f(); //6
上面的代碼定義了一個函數c,函數c內部定義了局部變量k,當函數c執行后返回了一個匿名函數,匿名函數訪問了局部變量k,變量f的值為這個匿名函數,調用f實際上是調用這個匿名函數。每次調用f(),變量k的值都會增加1。在這里局部變量k沒有在函數c執行完后被銷毀,反而“活”了下來,它的生存周期延長了。
什么是閉包?
當前作用域總是能夠訪問外部作用域中的變量, 函數是 JavaScript 中唯一擁有自身作用域的結構, 因此閉包的創建依賴於函數。
1. 一個函數可以引用外部函數的變量,這個函數就可算是一個閉包。
2. 外部函數已經執行完,內部的函數仍可以引用外部函數的變量。這個內部函數就可算是一個閉包。
3. 函數能存儲其作用域的變量、能讀寫當前函數作用域內變量的函數可算是一個閉包。