javascript 必知必會之closure

來源:互聯網
上載者:User

下面的代碼片斷縮排目前還不完善,你也可以選擇 下載pdf 來閱讀.

Contents

  • 摘要
  • 什麼是closure
  • 執行空間(執行內容, Execution Context)
  • closure的一些用法
  • 關於closure的效率
  • 應用建議
  • 結論
  • 參考資料
  • 本文的rst源碼
什麼是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. 具體的應用情境要熟悉

上篇博文講的是 prototype, 下篇博文預計會講 this關鍵字, 歡迎大家討論和留言.

參考資料
  1. JavaScript Closures
  2. Explaining JavaScript Scope And Closures
  3. JavaScript Closures 101
  4. JavaScript and memory leaks
  5. Closures in JavaScript
本文的rst源碼

本文的源碼連結在 這裡 .

本文中涉及的javascript代碼可以在 這兒 下載.

你也可以選擇 下載pdf 來閱讀.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.