javascript必知必會之closure

作者: Tower Joo  來源: 博客園  發布時間: 2009-10-16 17:31  閱讀: 1194 次  推薦: 0   原文鏈接   [收藏]  

  摘要

  本系列博文主要談一些在 javascript 使用中經常會混淆的高級應用,包括: prototype, closure, scope, this關鍵字. 對于一個需要提高自己javascript水平的程序員,這些都是必須要掌握的.本節主要介紹closure.

  什么是closure?

  一種定義是:A "closure" is an expression (typically a function) that can have free variables together with an environment that binds those variables (that "closes" the expression).

  我的理解是: closure 是一個表達式(通常是一個函數), 這個表達式與一個 環境 共享著一些自由變量, 而這個 環境 則 綁定 著那些自由變量(或者說 結束 這個表達式, 這也是所謂closure 的名字由來). 所謂的 環境 就是一個更大的block, 所有的自由變量在這個 block 中 聲明(有意義). 而 綁定 也就是指這些自由變量的作用域就是這個環境.

  舉個簡單的例子:

var flag = false;   //調試開關
// env 既是所謂的環境
// 而inner就是所謂的表達式, name即是所謂的自由變量
function env()  //整個env可以看作是一個closure
{
var name = "zhutao";
function inner()
{
return name + " is a student.";
}
return inner;   //返回的是一個內部函數
}//closure結束
flag = true;
if (flag)
{
// 此處是最神奇的地方, 代碼執行在此處, inner函數其實已經出了env的body,
// 而仍然能夠被引用, 這就是所謂形成了一個 closure
var inner_func_ref = env(); // 這時候inner_func_ref引用的就是inner()函數對象
alert(inner_func_ref());   // zhutao is a student.
}

  而在上面的例子中, 函數env就是所謂的定義中的 環境, 函數inner就是定義中所謂的 表達式, 而name即是所謂的 自由變量, 綁定 在env這個 環境 中. env的結束也即closure的結束.

  而在javascript中,如果內部函數出了自己的所在的外部函數的body仍然能夠引用,則會形成所謂的closure.

  在具體了解closure之前,我們需要了解一些其它的知識.

  執行空間(執行上下文, Execution Context)

  在 javascript 中,每行可執行的代碼都具有一定的 執行空間, 如全局的執行空間, 函數的執行空間, 遞歸后的函數執行空間等. 而一個完整的 javascript 執行過程,可以看作是有一個執行空間棧 ,不斷地 進行 執行空間 的變化(出棧,進棧).

  這個是很重要的概念,這個概念的理解與本系列的將要完成的另一篇文章 this關鍵字 的理解也是密切相關的.

  詳細解釋請參考即將完成的 this關鍵字 的博文.

  執行空間可以理解為具有屬性的對象集, 但是通常這些屬性都不是可隨意訪問的, 而這些對象集為代碼的執行 提供了一定的上下文(空間).

  當執行到一個函數時, 會建立此函數的執行空間(所謂進棧), 執行結束了, 從此執行空間退出返回到原來的執行空間(所謂 的出棧),而js解釋器在運行過程中一起維護著這樣一個 執行空間棧 來為不同的代碼提供不同的執行空間.

  那么執行空間與closure有什么關系?

  簡單地說,一定的執行空間對應著一定的closure, 只有位于同一個closure的方法才能訪問同一closure的變量.

  舉個簡單的例子:

// 關于context的例子
flag = true;
var tmpobj = {
name : "zhutao",
func : function(){
return "call by func " + this.name;
}
};
if (flag)
{
// 代碼執行在此處時context還是global
alert(tmpobj.name);
alert(tmpobj.func());   //進入func的context
// 回到global的context
}

  closure的一些用法:

  當內部函數和自由變量位于同一closure時,可以隨意訪問,而聲明順序并不重要.

  幾個常用的例子:

//一些應用
flag = true;
function OuterFun()
{
var num = 100;
var printNum = function(){alert(num);}  
//此處引用的num是引用,而不是值,所以后面改變num,此處的num同樣生效
num ++;
return printNum;
}
var myfunc = OuterFun();
myfunc();   //輸出的是101,而不是100
//另一個例子,下面的例子,可以看到匿名函數(內部函數)先于外部函數變量的聲明,
但是仍然能夠訪問外部函數的變量
// 也就是說內部函數與外部函數的變量位于同一個closure, 所以可以訪問
function SameClosure()
{
var iCanAccess = function(){alert(name);};
var name = "zhutao";
return iCanAccess;
}
var testSameClosure = SameClosure();
testSameClosure();// zhutao
// 另一個應用,關于module pattern, 這樣可以實際所謂的 private, public等方法和變量
var module = (function Module(){
var privateVar = "zhutao is private";   // private
return {
publicGetPrivateVar : function(){
return privateVar;
},  // public method, 可以取所謂的private變量
publicVar : "I'm a public variable" // public variable
};
})();
if (flag)
{
alert(module.publicGetPrivateVar());    // zhutao is private
alert(module.publicVar);                // I'm a public variable
alert(module.privateVar);               // undefined
}

  關于closure的效率

  因為在closure的實際應用可能會多次去生成一個內部函數(匿名),所以存在可能的效率問題.(對象的建立,內存管理釋放等).

  所以,應該盡量減少內部函數的生成, 而使用函數的引用.

  例如:

// 關于效率的例子
flag = false;
// 這樣,每次調用Outer時會產生匿名函數的開銷
function Outer(obj)
{
obj.fun = function(){
alert("I am " + this.name);
};
}
if (flag)
{
var obj = { name : "zhutao"};
Outer(obj);
obj.fun();
}
// 更好的處理方式
function Outer_better(obj)
{
obj.fun = showme;   // 這樣調用的只是函數的引用
}
function showme()
{
alert("I am " + this.name);
}
if (flag)
{
var obj2 = { name : "zhutao"};
Outer_better(obj2);
obj2.fun();
}

  應用建議

  Don't use closures unless you.really need closure semantics.In most cases, nonnested.functions are the right way to go.Eric Lippert, Microsoft

  上面的論述是基于效率的考慮, 而 IE 4-6 在使用closure時可能會存在內存泄露的問題,參考 JavaScript Closures 中的相關部分.

  而在某些場合,你可能必須要使用closure, 如 循環問題.

flag = true;
// 向body中生成一些鏈接,然后綁定事件
function addLink(num)
{
for(var i=0; i<num; i++)
{
var link = document.createElement('a');
link.innerHTML = "Link " + i;
link.onclick = function(){
alert(i);
};
document.body.appendChild(link);
}
}   //可惜的是,當你點擊每個鏈接時,輸出的都是 Link 4
// 使用closure 可以解決這個問題
function addLink2(num)
{
for(var i=0; i<num; i++)
{
var link = document.createElement('a');
link.innerHTML = "Link" + i;
link.onclick = function(j){ //使用closure
return function(){
alert(j);
};//返回一個函數
}(i);//調用這個函數
document.body.appendChild(link);
}
}
window.onload = addLink(4);
window.onload = addLink2(4);

  為什么會出現上面的這個問題?(事實在之前的的一個項目中,也遇到了相同的問題,但是當時還不懂closure, 也是一頭霧水)

  這是因為,對于addLink, 在退出addLink函數之前, i已經變成了4,所以無論后面的事件觸發,輸出的都是4.

  但是后者,使用了closure.使得j引用了當前的循環中的i,所以對于每個后續觸發事件,都會按照預期地得到相應的結果.

  具體的討論可見: SO這即是一個典型的closure應用場景, 而如果不使用, 就無法解決這個問題.

  結論

  下面這段摘抄自 Summary of JavaScript closures :

  1. 當你在一個函數中使用另一個函數時, 會產生一個closure
  2. 當你使用eval()時, 會產生一個closure.
     3. 最好認為closure總是在函數入口處產生,并且本地變量自動添加到closure中

  總之, 關于closure,你必須記住以下幾點:

  1. closure就是提供了一種變量共享的機制(內部函數可以訪問外部函數的變量)
  2. 注意closure可能引用的效率問題(如何避免,參見文中詳述)
  3. 具體的應用場景要熟悉

0
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()