閉包是指有權訪問另一個函數作用域的變量的函數。
閉包的局部變量可以在函數執行結束後仍然被函數外的代碼訪問。這意味著函數必須返回一個指向閉包的“引用”,或將這個”引用”賦值給某個外部變量,才能保證閉包中局部變量被外部代碼訪問。
在ECMAScript中,函數對象中定義的 內部函數(inner function) 是可以直接訪問外部函數的局部變量,創建閉包的常見方式,就是在一個函數內部創建另一個函數。
function welcome (name) {
var text = 'Hello ' + name; // local variable
// 每次調用時,產生閉包,並返回內部函數對象給調用者
return function () {
console.log(text);
}
}
var sayHello = welcome( "Viewer" );
sayHello() // 通過閉包訪問到了局部變量text
代碼的執行結果是:Hello Viewer,因為 sayHello() 函數在 welcome() 函數執行完畢後,仍然可以訪問到定義在其內部的局部變量 text ,這就是閉包的效果。
在ECMAscript的腳本的函數運行時,每個函數關聯都有一個執行上下文場景(Execution Context) ,這個執行上下文場景中包含三個部分:
文法環境(The LexicalEnvironment)
環境記錄(Enviroment Recode)
外部引用(指針)
變量環境(The VariableEnvironment)
局部變量
參數變量
this綁定
外部引用指向了外部函數對象的上下文執行場景。全局的上下文場景中此引用值為 NULL。這樣的數據結構就構成了一個單向的鏈表,每個引用都指向外層的上下文場景。
例如上面我們例子的閉包模型應該是這樣,sayHello 函數在最下層,上層是函數 welcome,最外層是全局場景。如下圖:
因此當sayHello被調用的時候,sayHello會通過上下文場景找到局部變量text的值,因此在屏幕的對話框中顯示出”Hello Viewer”。
變量環境(The VariableEnvironment)和文法環境的作用基本相似。
閉包實例
前面的內容大致說明了Javascript閉包是什麼,閉包在Javascript如何實現,下面通過針對一些例子來深入了解閉包。
例子1:閉包中局部變量是引用而非復制
function say667() {
var num = 666;
var sayAlert = function() {
console.log(num);
}
num++;
return sayAlert;
}
var sayAlert = say667();
sayAlert() //667
例子2:多個函數綁定同一個閉包,因為他們定義在同一個函數內。
function setupSomeGlobals () {
var num = 666;
// 存儲一些函數的引用作為全局變量
gAlertNumber = function() {
console.log(num);
}
gIncreaseNumber = function() {
num++;
}
gSetNumber = function(x) {
num = x;
}
}
setupSomeGlobals(); // 為三個全局變量賦值
gAlertNumber(); //666
gIncreaseNumber();
gAlertNumber(); // 667
gSetNumber(12);
gAlertNumber(); //12
例子3:當在一個循環中賦值函數時,這些函數將綁定同樣的閉包
<script type="text/javascript">
window.onload = function(){
var lists = document.getElementsByTagName("li");
for(var i=0,l=lists.length; i < l; i++){
lists[i].onclick = function(){
var t = i;
return function(){
console.log(t+1)
}
}()
}
}
</script>
<body>
<h1>當在一個循環中賦值函數時,這些函數將綁定同樣的閉包</h1>
<ul>
<li id="a1">aa</li>
<li id="a2">aa</li>
<li id="a3">aa</li>
</ul>
<body>
例子4:外部函數所有局部變量都在閉包內,即使這個變量聲明在內部函數定義之後。
function sayAlice() {
var sayAlert = function() {
console.log(alice);
}
var alice = 'Hello Alice';
return sayAlert;
}
var helloAlice = sayAlice();
helloAlice();
例子5:每次函數調用的時候創建一個新的閉包。
function newClosure(someNum, someRef) {
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
console.log('num: ' + num +
'\nanArray ' + anArray.toString() +
'\nref.someVar ' + ref.someVar);
}
}
closure1 = newClosure(40,{someVar:'closure 1'});
closure2 = newClosure(1000,{someVar:'closure 2'});
closure1(5); // num:45 anArray[1,2,3,45] ref:'someVar closure1'
closure2(-10);// num:990 anArray[1,2,3,990] ref:'someVar closure2'
由於閉包會攜帶包含它的函數的作用域,因此會比其它函數占用更多的內存,過度使用閉包會導致占用內存,所以只有在必要的時候再使用閉包。