只能是粗淺的,畢竟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精華,簡單說這裏有用的。函數的定義常見的是this
function fun1(val){ } fun1(5); var f1 = function(val){ }; // 定義一個函數賦給變量 f1(6); var f2 = function fun2(){ }; // 或者取一個函數名 f2();
由於函數也是對象、變量,因此能夠賦給一個變量(更準確說是賦給左值),在這個變量後面使用()調用運算符就能夠調用這個函數了,如f1()。還有一種方式:定義即調用google
var num = (function(val){ return val * val; }(5)); // num爲25
在定義一個函數體後面加上()和參數(或者沒有參數),就是對這個定義的函數進行了調用,直接傳入參數5計算並賦值給num,所以num是一個數值變量而不是函數變量。spa
既然函數是對象,固然也有new關鍵字的表達式code
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)進行復制一份,不會生成一個靜態快照,而是共享,當這個成員的值改變時,它們返回的值也跟着變化。
戰戰兢兢寫完,感受仍是要加深理解-_-