1.做用域鏈
javasrcipt的做用域鏈是很重要的一個環節,若是此處理解很差,在往後編寫javascript就會形成不少沒必要要的麻煩。固然若是可以很好的理解它,那麼javascript的閉包也能很好的理解了。在理解做用域鏈以前,首先要理解什麼是做用域。
做用域:直白點說,做用域就是變量和函數的訪問範圍,而且控制着變量和函數的可見性和生命週期。那麼在javascript中,做用域主要分爲兩種:
a)全局做用域
b)局部做用域
在其餘的語言中,好比c、c++、java等,在條件語句或者循環語句(即花括號以內)以內的每一段代碼都有各自的做用域,並且變量在聲明代碼段外是不可見的,稱之爲塊級做用域。可是在javascript中是沒有塊級做用域的,只有函數級做用域。請牢記這一點。咱們能夠看看下面的例子。
javascript
function fun(){ if(true){ var arg1 = '參數1'; } alert(arg1) for(var i=0;i<10;i++){ //什麼也不作 }; alert(i) }
執行上面代碼後就應該明白了,javascript並無塊級做用域。
變量沒有在函數內聲明或者聲明的時候沒有帶var就是全局變量,擁有全局做用域,window對象的全部屬性擁有全局做用域;在代碼任何地方均可以訪問,函數內部聲明而且以var修飾的變量就是局部變量,只能在函數體內使用,函數的參數雖然沒有使用var但仍然是局部變量。
java
var a=3; //全局變量 function fn(b){ //局部變量 c=2; //全局變量 var d=5; //局部變量 function subFn(){ var e=d; //父函數的局部變量對子函數可見 for(var i=0;i<3;i++){ console.write(i); } alert(i);//3, 在for循環內聲明,循環外function內仍然可見,沒有塊做用域 } } alert(c); //在function內聲明但不帶var修飾,仍然是全局變量
只要是理解了JavaScript沒有塊做用域,簡單的JavaScript做用域很好理解,還有一點兒容易讓初學者迷惑的地方是,JavaScript雖然是解釋執行,但也不是循序漸進逐句解釋執行的,在真正解釋執行以前,JavaScript解釋器會預解析代碼,將變量、函數聲明部分提早解釋,這就意味着咱們能夠在function聲明語句以前調用function,這多數人習覺得常,可是對於變量的與解析乍一看會很奇怪c++
console.log(a); //undefined var a=3; console.log(a); //3 console.log(b); //Uncaught ReferenceError: b is not defined
上面代碼在執行前var a=3; 的聲明部分就已經獲得預解析(可是不會執行賦值語句),因此第一次的時候會是undefined而不會報錯,執行過賦值語句後會獲得3,上段代碼去掉最後一句。
針對簡單的做用域總結一下:
1.沒有塊級做用域,只有函數級做用域。
2.JavaScript解釋器首先在當前做用域中搜索是否有該變量的定義,若是有,就是用這個變量;若是沒有就到父域中尋找該變量.以此類推,直到最頂級做用域,仍然沒有找到就拋出異常"變量未定義"。
3.全局執行環境是最外層的一個執行環境,在web瀏覽器中全局執行環境是window對象,所以全部全局變量和函數都是做爲window對象的屬性而建立的。每一個函數都有本身的執行環境,當執行流進入一個函數的時候,函數的環境會被推入一個函數棧中,而在函數執行完畢後執行環境出棧並被銷燬,保存在其中的全部變量和函數定義隨之銷燬,控制權返回到以前的執行環境中,全局的執行環境在應用程序退出(瀏覽器關閉)纔會被銷燬。
若是隻是這樣那麼JavaScript做用域問題就很簡單了,然而因爲函數子函數致使的問題使做用域不止這樣簡單。因此執行環境或者說運行期上下文登場了,即:執行環境(execution context)定義了變量或函數有權訪問的其它數據,決定了它們的各自行爲。每一個執行環境都有一個與之關聯的變量對象(variable object, VO),執行環境中定義的全部變量和函數都會保存在這個對象中,解析器在處理數據的時候就會訪問這個內部對象。
理解上面的內容後,再來理解做用域鏈
當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈(scope chain,不簡稱sc)來保證對執行環境有權訪問的變量和函數的有序訪問。做用域第一個對象始終是當前執行代碼所在環境的變量對象(VO)
web
function a(x,y){ var b=x+y; return b; }
在函數a建立的時候它的做用域鏈填入全局對象,全局對象中有全部全局變量
若是執行環境是函數,那麼將其活動對象(activation object, AO)做爲做用域鏈第一個對象,第二個對象是包含環境,下一個是包含環境的包含環境,以此類推。
數組
function a(x,y){ var b=x+y; return b; } var tatal=a(5,10);
這時候 var total=a(5,10);語句的做用域鏈以下
在函數運行過程當中標識符的解析是沿着做用域鏈一級一級搜索的過程,從第一個對象開始,逐級向後回溯,直到找到同名標識符爲止,找到後再也不繼續遍歷,找不到就報錯。
下面以繪製的方法深刻理解一下做用域鏈。
1 繪製規則:
1) 做用域鏈就是對象的數組
2) 所有script是0級鏈,每一個對象佔一個位置
3) 凡是看到函數延伸一個鏈出來,一級級展開
4) 訪問首先看當前函數,若是沒有定義往上一級鏈檢查
5) 如此往復,直到0級鏈
2 舉例
var num = 10;
var func1 = function() {
var num = 20;
var func2 = function() {
var num = 30;
alert(num);
};
func2();
};
var func2 = function() {
var num = 20;
var func3 = function() {
alert(num);
};
func3();
};
func1();
func2();
下面分析一下這段代碼:
-> 首先整段代碼是一個全局做用域,能夠標記爲0級做用域鏈,那麼就有一個數組
var link_0 = [ num, func1, func2 ];// 這裏用僞代碼描述
-> 在這裏func1和func2都是函數,所以引出兩條1級做用域鏈,分別爲
var link_1 = { func1: [ num, func2 ] };// 這裏用僞代碼描述
var link_1 = { func2: [ num, func3 ] };// 這裏用僞代碼描述
-> 第一條1級鏈衍生出2級鏈
var link_2 = { func2: [ num ] };// 這裏用僞代碼描述
-> 第二條1級鏈中沒有定義變量,是一個空鏈,就表示爲
var link_2 = { func3: [ ] };
-> 將上面代碼整合一下,就能夠將做用域鏈表示爲:
複製代碼 代碼以下:
// 這裏用僞代碼描述
var link = [ // 0級鏈
num,
{ func1 : [ // 第一條1級鏈
num,
{ func2 : [ // 2級鏈
num
] }
]},
{ func2 : [ // 第二條1級鏈
num,
{ func3 : [] }
]}
];
有了這個做用域鏈的圖,那麼就能夠很是清晰的瞭解訪問變量是如何進行的:
在須要使用變量時,首先在當前的鏈上尋找變量,若是找到就直接使用,不會向上再找;若是沒有找到,那麼就向上一級做用域鏈尋找,直到0級做用域鏈.
2.閉包
概念:閉包是指有權訪問另外一個函數做用域中的變量的函數。
建立閉包的常見方式就是在一個函數內部建立另外一個函數。
當內部函數在定義它的做用域的外部被引用時,就建立了該內部函數的一個閉包。這種狀況下咱們稱既不是內部函數局部變量,也不是其參數的變量爲自由變量,稱外部函數的調用環境爲封閉閉包的環境。從本質上講,若是內部函數引用了位於外部函數中的變量,至關於受權該變量可以被延遲使用。所以,當外部函數調用完成後,這些變量的內存不會被釋放(最後的值會保存),閉包仍然須要使用它們。
瀏覽器
function outerFn() { var outerVar = 0; function innerFn() { outerVar++; console.log("outerVar = " + outerVar + "<br/>"); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();
閉包之間的交互
當存在多個內部函數時,極可能出現意料以外的閉包。咱們定義一個遞增函數,這個函數的增量爲2
閉包
function outerFn() { var outerVar = 0; function innerFn1() { outerVar++; console.log("Inner function 1 outerVar = " + outerVar + "<br/>"); } function innerFn2() { outerVar += 2; console.log("Inner function 2 outerVar = " + outerVar + "<br/>"); } return { "fn1": innerFn1, "fn2": innerFn2 }; } var fnRef = outerFn(); fnRef.fn1(); fnRef.fn2(); fnRef.fn1(); var fnRef2 = outerFn(); fnRef2.fn1(); fnRef2.fn2(); fnRef2.fn1();
咱們映射返回兩個內部函數的引用,能夠經過返回的引用調用任一個內部函數,結果:
Inner function 1 outerVar = 1
Inner function 2 outerVar = 3
Inner function 1 outerVar = 4
Inner function 1 outerVar = 1
Inner function 2 outerVar = 3
Inner function 1 outerVar = 4
innerFn1和innerFn2引用了同一個局部變量,所以他們共享一個封閉環境。當innerFn1爲outerVar遞增一時,久違innerFn2設置了outerVar的新的起點值,反之亦然。咱們也看到對outerFn的後續調用還會建立這些閉包的新實例,同時也會建立新的封閉環境,本質上是建立了一個新對象,自由變量就是這個對象的實例變量,而閉包就是這個對象的實例方法,並且這些變量也是私有的,由於不能在封裝它們的做用域外部直接引用這些變量,從而確保了了面向對象數據的專有性。
解惑
如今咱們能夠回頭看看開頭寫的例子就很容易明白爲何第一種寫法每次都會alert 4了。函數
for (var i = 0; i < 4; i++) { spans[i].onclick = function() { alert(i); } }
上面代碼在頁面加載後就會執行,當i的值爲4的時候,判斷條件不成立,for循環執行完畢,可是由於每一個span的onclick方法這時候爲內部函數,因此i被閉包引用,內存不能被銷燬,i的值會一直保持4,直到程序改變它或者全部的onclick函數銷燬(主動把函數賦爲null或者頁面卸載)時纔會被回收。這樣每次咱們點擊span的時候,onclick函數會查找i的值(做用域鏈是引用方式),一查等於4,而後就alert給咱們了。而第二種方式是使用了一個當即執行的函數又建立了一層閉包,函數聲明放在括號內就變成了表達式,後面再加上括號括號就是調用了,這時候把i當參數傳入,函數當即執行,num保存每次i的值。
備註:在方法的方法中調用this的話默認指向的都是全局做用域,即window
以下例子
this
var a = { b:function(){ alert(this); (function(_this){ alert(this); alert(_this); })(this); } } a.b();