只能是粗淺的,畢竟js用法太靈活。
首先拋概念:閉包(closure)是函數對象與變量作用域鏈在某種形式上的關聯,是一種對變量的獲取機制。這樣寫鬼能看懂。
所以要大致搞清三個東西:函數對象(function object)、作用域鏈(scope chain)以及它們如何關聯(combination)
首先要建立一個印象,在js中,幾乎所有的東西可以看作對象,除了null和undefined。比如常用的數組對象、日期對象、正則對象等。
var num = 123; // Number var arr = [1,2,3]; // Array var str = "hello"; // String
數字原始值可以看作Number對象,字符串原始值可看做String對象,數組原始值可看作Array對象。有的原始值還可直接調方法,如數組、正則,有的不行
[1,2,3].toString(); // 可以 /\w+/.toString(); // 可以 123.toString(); // 報錯
比如數字,當用一個變量名呈接它時,或者加上括號,能夠這樣用
var num = 123; num.toString(); 123.toString(); // 報錯,因解釋器當成浮點數來解讀 (123).toString(); // 可以
所以,函數也可以是一個對象,函數細說起來又要扯一大堆,畢竟是js精華,簡單說這里有用的。函數的定義常見的是
function fun1(val){ } fun1(5); var f1 = function(val){ }; // 定義一個函數賦給變量 f1(6); var f2 = function fun2(){ }; // 或者取一個函數名 f2();
因為函數也是對象、變量,所以可以賦給一個變量(更准確說是賦給左值),在這個變量后面使用()調用運算符就可以調用這個函數了,如f1()。還有一種方式:定義即調用
var num = (function(val){ return val * val; }(5)); // num為25
在定義一個函數體后面加上()和參數(或者沒有參數),就是對這個定義的函數進行了調用,直接傳入參數5計算並賦值給num,因此num是一個數值變量而不是函數變量。
既然函數是對象,當然也有new關鍵字的表達式
var a = new Array(1,2,3); // 數組的對象創建表達式 var f = new Function("x", "y", "x = 2 *x; return x + y;"); // 函數對象創建表達式 var f1 = function(x, y){ // 函數f的函數體類似f1的定義 x = 2 * x; return x + y; };
函數對象的創建使用了Function關鍵字,前面的參數均被當做對象創建函數的形參,如這里的x、y,最后一個字符串是函數的函數體,多行函數體仍以;相隔。
很多時候特別是使用jQuery時經常看到函數調用時傳遞函數,大多數時候直接寫匿名函數,也可可傳遞一個函數變量
func(index, function(val){ /* 匿名函數 */ }); var f = function(val){ /* 函數變量 */ }; func1(index, f);
既然函數函數是對象,可以賦給一個變量,自然也可以作為返回值了,而且在js中,函數可以嵌套定義。
function func(){ return function(x){ // 返回一個函數變量 return x * x; } } var f = func(); f(5); // 對這個函數進行調用
function func1(){ function nested1(){ } // 嵌套定義函數 function nested2(){ } }
對函數有個大致了解,說說變量作用域問題,有幾個原則:
1. 全局變量擁有全局作用域.
2. 局部變量(一般指定義在函數內部)擁有局部作用域(包括其嵌套的函數).
3. 在局部變量若跟全局變量重名,優先使用局部變量.
val = 'value'; // 變量定義可以不用var關鍵字 document.write("val=>" + val + "<br/>"); // g是全局變量,在全局作用域中有效,所以在給g初始化之前就可以訪問,只是值是undefined document.write("g=>" + g ); var g = 'google';
全局變量的定義,相當於是全局對象的屬性,一般我們用this指代這個全局對象,比如在瀏覽器中運行的時候,它指的是window對象,即當前窗體,在全局作用域中,以下三種訪問形式等效
var g = 'google'; document.write("g=>" + g + "<br/>"); document.write("g=>" + this.g + "<br/>"); document.write("g=>" + window.g + "<br/>");
而在函數定義內部訪問變量時,遵循同名優先,函數作用域內部總是優先訪問,例如
var scope = "global"; function func1(){ var scope = "local"; console.log(scope); // local }; func1(); function func2(){ scope = 'changed global'; // 如不加var,改變的是全局變量的值 }; func2(); console.log(scope); // changed global
比較有意思的地方是,如果在一個函數內部給一個全局變量賦值時沒有加var關鍵字,如func2,它改變的是全局作用域變量的值!而前面說的this,也有個有意思的地方
var scope = "global"; function func(){ var scope = "local" console.log(scope); // local console.log(this.scope); // global } func();
在局部作用域(這里均指函數內部),如果有同名變量,以this引用的話,結果是全局變量,即,在函數內部(注意不是方法,方法一般指對象的屬性方法),this指代的是全局的對象。再看一個
var scope = "global"; function func(){ "use strict"; // 開啟ECMAScript5嚴格模式 console.log(this); // undefined console.log(this.scope); // 報錯:TypeError: scope undefined } func();
在嚴格模式中,對語法檢查更加嚴格。在一個全局作用域定義的普通函數中,log打印this是undefined,所以this引用scope當然也不存在。
在ECMAScript(為js腳本制定的一個標准)中,強制規定全局作用域定義的變量,是全局對象(比如在瀏覽器客戶端運行時為window,默認的全局中的this關鍵字也是指它,通常我們說的就是這個)的屬性。一般我們把跟某個變量作用域相關的對象為上下文環境(context),比如全局對象this只要涉及環境這類偏底層的東西肯定就是編程語言層面自己規定的。
但這個全局對象this限於非嚴格模式的情況。js允許我們在局部變量的環境中(函數)以this引用全局對象,在嚴格模式下卻沒法這樣干,也許是它把這個對象給隱藏了,至少目前這樣寫會報錯。
正是js的函數有局部作用域的特殊功能---全局作用域無法訪問函數中定義的變量,所以在js中,也用函數規定命名空間,相比其他有的語言使用的是namespace關鍵字,js目前好像是把namespace作為保留字,你的變量的命令不能跟它重名,但沒投入使用。
(function(){ var name = "Jeff"; var age = 28; var pos = "development"; }()); // 在函數中定義一堆變量,外邊無法訪問
在一個函數中定義一堆變量,當然得調用它才能生效,這堆變量就限於在這個空間內使用了,好像用得也不多。
一個重要的點,一個函數中規定的變量,在這個函數內部所有地方都可訪問,包括嵌套函數。所以可以出現下面這個現象
function func(){ console.log(num); // undefined,先於定義訪問 var num = 123; console.log(num); // 123 } func();
這種特性有時被稱為聲明提前(hoisting),相當於這樣
function func(){ var num; console.log(num); // undefined,先於定義訪問 var num = 123; console.log(num); // 123 }
然后是嵌套函數,只要在函數內定義的,該函數內均能訪問,看例子
function func(){ var str = "hello"; function nested1(){ // 第一次嵌套 str = "nested1"; function nested2(){ str = "nested2"; } // 第二次嵌套 nested2(); } nested1(); console.log(str); } func(); // 打印nested2
除了明確在函數內定義的變量,還有定義函數時的形參,它們在整個函數內也是可訪問的。
現在我們大概了解函數也是對象,以及全局、局部作用域,在全局作用域定義的變量,是對應的全局對象的屬性,也就是說有個全局對象關聯它,就從這點進入作用域鏈(scope chain)吧,這是理解閉包的基礎。
在一個局部作用域內,或者說定義的函數,想象它們關聯着某個對象,這個對象是隨着我們定義這個函數而自動生成的,函數內定義的變量以及函數的形參均是這個對象的屬性,所以在這個對象內部總是可以順利訪問到它們,類似於全局變量是全局對象的屬性。這種對象關聯在定義時就已經決定了,而不是在調用時才形成(這很重要)。
但是我們定義的很多函數都是嵌套的,由外到內每個函數都會有一個對應的自定義對象跟它關聯
var a = "a"; var name = "Michel"; function fun(b){ var c = "c"; var name = "Clark"; function nested(d){ var name = "Bruce"; var e = "e"; /* TODO */ } /* TODO */ }
在上面的嵌套函數nested生成一個自定義對象時,fun函數、全局作用域也會生成對象,因此它們可以形成一個對象的列表或者鏈表,簡單的將函數名作為對象名,全局對象用global表示,並且從內嵌函數nested函數出發的話,大概是這樣:

列表上的一組對象定義了這段代碼作用域中定義過的變量:即它們的屬性。第一個對象的屬性是當前函數的形參與內定義變量,第二個對象是它的外部函數的形參與內定義變量,一直到最后是全局對象和它的屬性---全局定義的變量,也就是說,當前函數永遠在這個列表的最前面,這樣才可以保證該函數范圍內的變量總是具有最訪問高優先級。每次訪問變量時便會順着這個列表查找,這被稱為變量解析(variable resolution),如果一直找到列表末尾都找不到對象中的這個屬性,會拋一個ReferenceError錯誤。
每個定義的函數對象都會有類似這樣一個列表與之關聯,它們之間通過這個作用域鏈相關聯,而函數體內定義的變量均可保存在函數作用域內,這是在函數定義時及確定的,這種特性稱之為閉包。 通常直接把函數稱作閉包,而且理論上來說所有的js函數都是閉包,因為它們都是對象,閉包這種機制讓js有能力來“捕捉”變量。第一個例子:
var scope = "global"; function func(){ var scope = "local"; function nested(){ return scope; } return nested(); // 調用並返回scope } console.log(func());
常規的定義和調用,嵌套函數的定義並調用在局部作用域中完成。它打印的是local。再看這個
var scope = "global"; function func(){ var scope = "local"; // 局部變量 function nested(){ return scope; } return nested; // 返回這個嵌套函數變量 } console.log(func()()); // 在全局作用域中調用局部的嵌套函數
前面說過,函數也是變量、對象,也可以作為函數返回值,func不直接返回變量,而返回一個內嵌函數,然后在全局作用域調用這個局部內嵌函數,它會返回什么呢?結果仍為local。由於閉包機制,nested函數在定義時就已經決定了,函數體內的scope變量值是local,這是一種綁定關系,不會隨着調用環境的改變而改變,它去對象關聯列表中查找按優先級分的話,總是func函數對應的scope值。
閉包的功能很強大,看這個例子(例A)
var integer = (function(){ var i = 0; return function(){ return i++; }; }()); console.log(integer()); // 0 console.log(integer()); // 1 console.log(integer()); // 2
如果有點C/C++基礎的人,初看這個調用結果,很可能會說這個打印的是0,0,0,反正我是很小白的這樣想,每次調用完,臨時變量i就會被銷毀。但是確實有打印0的情況,看下這個(例B)
function func(){ var i = 0; function nested(){ return i++; } return nested(); } console.log(func()); // 0 console.log(func()); // 0 console.log(func()); // 0
也就是說,這兩個看起來差不多的函數還是有點差別的。
在C/C++中,如果不是全局變量或局部靜態變量,只要在局部函數中定義的變量在調用一次完成后就馬上被銷毀,當然除使用malloc、realloc、new等函數開辟的動態空間除外,這種必須得手動釋放,否則容易造成內存泄露。js中是垃圾自動回收機制,某些無用的東西會被自動銷毀,在前兩個例子中,例A顯然沒有被銷毀,而例B中的變量被銷毀了,因為每次調用都是新聲明一個i變量。so why?
在C中,局部變量被臨時保存在一個棧中,先調用的先入棧,后調用的后入棧,調用完從棧訂彈出,變量內存被銷毀,利用的是棧的后進先出特點。而js依靠的是作用域鏈,這是一個列表或者鏈表,並不是棧,沒有所謂的壓入(push)、彈出(pop)操作,如果說定義時就有一個列表的話,每次調用一個函數時,都會創建一個新的、跟它關聯的對象,保存着局部變量,然后把這些對象添加至一個列表中形成作用域鏈,即便調用同一個函數兩次,生成也是兩個列表。
當一個函數執行完要返回的時候,便把對應對象從列表中刪除,對象中的屬性也會被銷毀,意味着局部函數中的變量將不復存在,在例B中,return nested(),執行完返回一個值,nested函數再無任何作用,被從列表中刪掉了。
如果一個局部函數定義了嵌套函數,並且有一個外部引用指向這個嵌套函數,就不會被當做垃圾回收。什么時候會有一個外部引用指向它(內嵌函數)?當它作為返回值(即返回一個函數變量),或者它作為某個對象屬性的值存儲起來時。不會被當成垃圾回收,它綁定的對象也不會從對象列表中刪掉,這個綁定對象的屬性和值自然也不會被銷毀,自然可以進行復用了。所以再次調用其創建一個新的對象列表時,變量的值是在上一次調用的基礎上改變的。
例A返回的是一個函數變量,意味着有一個外部引用指向着它:Hey!你可能會被調用哦,我不會刪除你的。這樣每次i是累加的。類似例A,如果將函數保存為一個對象的屬性也不會被刪除,例C
function counter(){ var n = 5; return { count: function(){ return n++; }, reset: function(){ n = 0; } }; } var countA = counter(); console.log(countA.count()); // 5 console.log(countA.count()); // 6 console.log(countA.count()); // 7
例C返回一個對象,對象的屬性均是函數,也符合上邊的情形,所以n值是累加的。這些情況下,通常我們會把類似定義的n稱為這個外部函數的私有屬性(成員),因為它們運行起來就像是函數的內嵌函數(閉包)共享的東西一樣。前面說過,我們有時直接將函數稱作閉包,尤其是同一個函數內部定義的函數
function func(){ var funArr = []; for(var i = 0; i <= 2; ++i) funArr[i] = function(){ return i; }; return funArr; } var farr = func(); console.log(farr[0]()); console.log(farr[1]()); console.log(farr[2]());
可以說:Here we create three closures, they are all defined in one function, so they share access to the variable i。那么它們輸出神馬?事實是,它們都打印3。它們共享變量,當func執行完時,i的值為3,所有的閉包均共享這個值。這個例子說明閉包的一個重要特點:所有閉包在與作用域鏈關聯時是“活動的”,雖然函數一定義完成,作用域鏈就隨着生成,但是所有閉包均不會單獨對作用域內的私有成員(如上例中的i、例C中的n)進行復制一份,不會生成一個靜態快照,而是共享,當這個成員的值改變時,它們返回的值也跟着變化。
戰戰兢兢寫完,感覺還是要加深理解-_-
