本篇的標題雖然是"jQuery閉包之淺見...",但實際上本篇涉及的多半是javascript"閉包"相關內容,之所以寫這個標題,完全是因為自己平常用jQuery寫前端習慣了。還有一個原因是:javascript"閉包"很容易造成"內存泄漏", 而jQuery已經自動為我們規避、處理了由"閉包"引發的"內存泄漏"。
javascript的"閉包"是這樣定義的:一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
定義高度提煉,還是從代碼實例來體驗"閉包"的來龍去脈吧。本篇主要包括:
從面向對象的角度來理解"閉包"
在面向對象中,我們是這樣定義類:
public class SomeClass
{
private string _someVariable;
public void SomeMethod()
{
//TODO:
}
}
在javascript中,有一種情況是,一個函數中包含了另外一個內部函數:
function OutFunction()
{
var temp = 0;
function innerFunction(){
}
}
我們把OutFunction稱為外部函數,把innerFunction稱為內部函數。
這里的外部函數OutFunction相當於一個類。
外部函數變量temp相當於類的私有字段。
內部函數innerFunction相當於類的方法。
就像我們需要實例化類來調用類的實例方法一樣,在javasript中,當我們在外部函數之外調用內部函數的時候,我們把此時的內部函數叫做"閉包",相當於面向對象的實例方法。
接下來,從"如何調用內部函數"這個角度,來循序漸進地體會何謂"閉包"。
調用內部函數的幾種方式
□ 在外部函數之外直接調用內部函數
<script type="text/javascript">
function outerFunction() {
console.log('外部函數執行了');
function innerFunction() {
console.log('內部函數執行了');
}
}
$(function() {
innerFunction();
});
</script>
結果報錯:Uncaught ReferenceError: innerFunction is not defined
以上情況,內部函數的作用域是在外部函數之內,當在外部函數之外調用內部函數的時候,自然就報錯。
□ 在外部函數之內調用內部函數:
<script type="text/javascript">
function outerFunction() {
console.log('外部函數執行了');
function innerFunction() {
console.log('內部函數執行了');
}
innerFunction();
}
$(function() {
outerFunction();
});
</script>
結果:
外部函數執行了
內部函數執行了
以上情況,內部函數的作用域是在外部函數之內,當在外部函數之內調用內部函數,自然不會報錯。
□ 在外部函數之外調用內部函數,把內部函數賦值給一個全局變量
<script type="text/javascript">
//全局變量
var globalInner;
function outerFunction() {
console.log('外部函數執行了');
function innerFunction() {
console.log('內部函數執行了');
}
globalInner = innerFunction;
}
$(function() {
outerFunction();
console.log('以下是全局變量調用內部方法:');
globalInner();
});
</script>
結果:
外部函數執行了
以下是全局變量調用內部方法:
內部函數執行了
以上情況,全局變量globalInner相當於面向對象的委托,當把內部函數賦值給全局變量,調用委托方法就會調用內部函數。
□ 在外部函數之外調用內部函數,把內部函數賦值給一個變量:
<script type="text/javascript">
function outerFunction() {
console.log('外部函數執行了');
function innerFunction() {
console.log('內部函數執行了');
}
return innerFunction;
}
$(function() {
console.log('先把外部函數賦值給變量');
var temp = outerFunction();
console.log('再執行外部函數變量');
temp();
});
</script>
結果:
先把外部函數賦值給變量
外部函數執行了
再執行外部函數變量
內部函數執行了
以上情況,我們可以看到,內部函數不僅可以賦值給全局變量,還可以賦值給局部變量。
就像面向對象的方法會用到類的字段,內部函數也會用到變量,接下來體驗變量的作用域。
變量的作用域
□ 內部函數變量
<script type="text/javascript">
function outerFunction() {
function innerFunction() {
var temp = 0;
temp++;
console.log('內部函數的變量temp的值為:' + temp);
}
return innerFunction;
}
$(function() {
var out1 = outerFunction();
out1();
out1();
var out2 = outerFunction();
out2();
out2();
});
</script>
結果:
內部函數的變量temp的值為:1
內部函數的變量temp的值為:1
內部函數的變量temp的值為:1
內部函數的變量temp的值為:1
從中我們可以看出內部函數變量temp的生命周期:
→當第一次執行內部函數,JavaScript運行時創建temp變量
→當第二次執行內部函數,JavaScript垃圾回收機制把先前的temp回收,並釋放與該temp對應的內存,再創建一個新的內部函數變量temp
.....
所以,每次調用內部函數,內部函數的變量是全新的。也就是說,內部函數的變量與內部函數同生共滅。
□ 全局變量
<script type="text/javascript">
//全局變量
var temp = 0;
function outerFunction() {
function innerFunction() {
temp++;
console.log('全局變量temp的值為:' + temp);
}
return innerFunction;
}
$(function() {
var out1 = outerFunction();
out1();
out1();
var out2 = outerFunction();
out2();
out2();
});
</script>
結果:
全局變量temp的值為:1
全局變量temp的值為:2
全局變量temp的值為:3
全局變量temp的值為:4
可見,全局變量供外部函數和內部函數共享。
□ 外部函數變量
<script type="text/javascript">
function outerFunction() {
var temp = 0;
function innerFunction() {
temp++;
console.log('外部函數變量temp的值為:' + temp);
}
return innerFunction;
}
$(function() {
var out1 = outerFunction();
out1();
out1();
var out2 = outerFunction();
out2();
out2();
});
</script>
結果:
外部函數變量temp的值為:1
外部函數變量temp的值為:2
外部函數變量temp的值為:1
外部函數變量temp的值為:2
可見,外部函數的變量與外部函數同生共滅。
以上情況,更接近與"閉包"的原型。有如下幾個要素:
1、外部函數
2、外部函數變量
3、內部函數
當我們在外部函數之外調用內部函數的時候,這時的內部函數就叫做"閉包",可以理解為面向對象的實例方法。"閉包"與外部函數變量的"外部環境"是外部函數,他倆與外部函數同生共滅。
一個外部函數中可以有多個內部函數,當調用"閉包"的時候,多個"閉包"共享外部函數變量:
<script type="text/javascript">
function outerFunction() {
var temp = 0;
function innerFunction1() {
temp++;
console.log('經innerFunction1,外部函數變量temp的值為:' + temp);
}
function innerFunction2() {
temp += 2;
console.log('經innerFunction2,外部函數變量temp的值為:' + temp);
}
return {'fn1' : innerFunction1, 'fn2' : innerFunction2};
}
$(function() {
var out1 = outerFunction();
out1.fn1();
out1.fn2();
out1.fn1();
var out2 = outerFunction();
out2.fn1();
out2.fn2();
out2.fn1();
});
</script>
結果:
經innerFunction1,外部函數變量temp的值為:1
經innerFunction2,外部函數變量temp的值為:3
經innerFunction1,外部函數變量temp的值為:4
經innerFunction1,外部函數變量temp的值為:1
經innerFunction2,外部函數變量temp的值為:3
經innerFunction1,外部函數變量temp的值為:4
jQuery中的"閉包"
"閉包"在jQuery中最常見的應用是,當Document加載完畢再執行jQuery部分:
<script type="text/javascript">
$(document).ready(function() {
var temp = 0;
function innerFunction() {
temp++;
console.log(temp);
}
innerFunction();
innerFunction();
});
</script>
結果:
1
2
可見,$(document).ready()的參數就是一個匿名外部函數,匿名函數內的函數是內部函數。
把元素的事件也可看作是內部函數:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
<script src="../Scripts/jquery-2.1.1.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
var counter = 0;
$('#btn1').click(function(event) {
counter++;
console.log(counter);
});
$('#btn2').click(function(event) {
counter--;
console.log(counter);
});
});
</script>
</head>
<body>
<input id="btn1" type="button" value="遞增"/>
<input id="btn2" type="button" value="遞減"/>
</body>
可見,2個input元素的click事件看作是內部函數,共享同一個外部函數變量counter。
在循環體遍歷中,把每次遍歷的元素事件也可看作是內部函數:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
<script src="../Scripts/jquery-2.1.1.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
for (var i = 0; i < 3; i++) {
$('<div>Print ' + i + '</div>').click(function() {
console.log(i);
}).insertBefore('#results');
}
});
</script>
</head>
<body>
<div id="results"></div>
</body>
頁面呈現的結果如預期:
Print 0
Print 1
Print 2
可當點擊每個div的時候,原本以為控制器台應該顯示:0, 1, 2,但實際顯示的始終是3,為什么?
--i看作是匿名外部函數的"自由變量",當頁面加載完畢后,i就變成了3。div的每次點擊看作是內部函數的閉環,而所有的閉環都共享了值為3的這個變量。
我們可以使用jQuery的each()方法來解決以上問題,遍歷一個數組,每一次遍歷元素值都不同:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
<script src="../Scripts/jquery-2.1.1.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$.each([0, 1, 2], function(index, value) {
$('<div>Print ' + value + '</div>').click(function() {
console.log(value);
}).insertBefore('#results');
});
});
</script>
</head>
<body>
<div id="results"></div>
</body>
內存泄漏
內存泄漏可以狹隘地理解為:應用程序中變量對應一塊內存,由於某些原因(比如代碼上的漏洞),始終沒有對變量及時實施手動或自動垃圾回收,內存沒有得到及時釋放,造成內存泄漏。
在JavaScript中,如果對象間的關系比較簡單:
以上,A有一個屬性指向B,B有一個屬性指向C,當A被回收的時候,沒有依賴B的對象,B隨之被自動回收,C也被最終回收。
當對象間的關系比較復雜,比如存在循環引用的時候,如下:
以上,A有一個屬性指向B, B有一個屬性指向C,而C又有一個屬性指向B,B和C之間存在循環引用。當A被回收之后,B和C是無法自動被回收的,在JavaScript中應該手動處理回收。
JavaScript閉包有可能會造成內存泄漏:
→內部函數閉包引發的內存泄漏
$(function() {
var outerVal = {};
function innerFunction() {
console.log(outerVal);
}
outerVal.fn = innerFunction;
return innerFunction;
});
以上,outVal是在內存中的一個對象,而內部函數innerFunction同樣是內存中的一個對象。對象outVal的屬性fn指向內部函數,而內部函數通過console.log(outerVal)引用outVal對象,這樣outVal和內部函數存在循環引用的情況,如果不手動處理,就會發生"內存泄漏"。
如果,我們在內部函數中不顯式引用outerVal對象變量,會造成"內存泄漏"嗎?
$(function() {
var outerVal = {};
function innerFunction() {
console.log('hello');
}
outerVal.fn = innerFunction;
return innerFunction;
});
答案是:會的。因為,當內部函數被引用、調用的時候,即使內部函數沒有顯式引用外部函數的變量,也會隱式引用外部函數變量。
→元素事件引發的內存泄漏
在IE中,如下寫法會造成內存泄漏:
$(document).ready(function(){
var button = document.getElementById("btn");
button.onclick = function(){
console.log('hello');
return false;
}
});
而如下JavaScript寫法不會造成內存泄漏:
function hello(){
console.log('hello');
return false;
}
$(document).ready(function(){
var button = docuemtn.getElementById('btn');
button.onclick = hello;
});
而在jQuery中,類似的寫法就不用擔心內存泄漏了,因為jQuery為我們做了自動處理來規避內存泄漏。
$(document).ready(function(){
var $button = $('#btn');
$button.click(function(event){
event.preventDefault();
console.log('hello');
});
});
總結
與"閉包"相關的包括:變量的作用域、javascript垃圾回收、內存泄漏,需在實踐多體會。


