任何程序設計語言都有做用域的概念,簡單的說,做用域就是變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期。在JavaScript中,變量的做用域有全局做用域和局部做用域兩種前端
JS中的做用域是基於上下文,以函數劃分的,而不是由塊(block)劃分的git
全局做用域數組
以下:瀏覽器
var a = 111; // 全局變量 做用域:函數內外都能訪問
function f(){
var b = 666; //局部變量
}
複製代碼
以下:bash
var a = 111; // 全局變量 做用域:函數內外都能訪問
function f(){
var b = 666; //局部變量
c = 777; //全局變量
}
複製代碼
全部window對象的屬性擁有全局做用域閉包
通常狀況下,window對象的內置屬性都都擁有全局做用域,例如window.name、window.location、window.top等等函數
局部做用域ui
和全局做用域相反,局部做用域通常只在固定的代碼片斷內可訪問到,最多見的例如函數內部,因此在一些地方也會看到有人把這種做用域稱爲函數做用域 (如上述代碼中,函數 f 內部就被稱爲局部做用域)this
(若是想了解更多,請參考個人JavaScript函數變量的使用)spa
瞭解完做用域後,接下來就要說咱們的做用域鏈了
若是想要知道JS怎麼鏈式查找,就必須先要了解JS的執行環境
執行環境(execution context)
每一個函數運行時都會產生一個執行環境,js爲每個執行環境關聯了一個變量對象。環境中定義的全部變量和函數都保存在這個對象中。
全局執行環境是最外圍的執行環境,全局執行環境被認爲是window對象,所以全部的全局變量和函數都做爲window對象的屬性和方法建立的。
js的執行順序是根據函數的調用來決定的,當一個函數被調用時,該函數環境的變量對象就被壓入一個環境棧中。而在函數執行以後,棧將該函數的變量對象彈出,把控制權交給以前的執行環境變量對象。
例如:
<script>
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
</script>
複製代碼
上面代碼執行狀況以下圖所示:
瞭解了環境變量,就下來再詳細講講做用域鏈。
當某個函數第一次被調用時,就會建立一個執行環境(execution context)以及相應的做用域鏈,並把做用域鏈賦值給一個特殊的內部屬性([scope])。而後使用this,arguments(arguments在全局環境中不存在)和其餘命名參數的值來初始化函數的活動對象(activation object)。當前執行環境的變量對象始終在做用域鏈的第0位。
以上面的代碼爲例,當第一次調用fn1()時的做用域鏈以下圖所示(由於fn2()尚未被調用,因此沒有fn2的執行環境):
能夠看到fn1活動對象裏並無scope變量,因而沿着做用域鏈(scope chain)向後尋找,結果在全局變量對象裏找到了scope,因此就返回全局變量對象裏的scope值。
標識符解析是沿着做用域鏈一級一級地搜索標識符地過程。搜索過程始終從做用域鏈地前端開始,而後逐級向後回溯,直到找到標識符爲止(若是找不到標識符,一般會致使錯誤發生)
做用域鏈地做用不只僅只是爲了搜索標識符
再來看一段代碼:
<script>
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn();
</script>
複製代碼
outer()內部返回了一個inner函數,當調用outer時,inner函數的做用域鏈就已經被初始化了(複製父函數的做用域鏈,再在前端插入本身的活動對象),具體以下圖:
通常來講,當某個環境中的全部代碼執行完畢後,該環境被銷燬(彈出環境棧),保存在其中的全部變量和函數也隨之銷燬(全局執行環境變量直到應用程序退出,如網頁關閉纔會被銷燬)
可是像上面那種有內部函數的又有所不一樣,當outer()函數執行結束,執行環境被銷燬,可是其關聯的活動對象並無隨之銷燬,而是一直存在於內存中,由於該活動對象被其內部函數的做用域鏈所引用。
具體以下圖:
outer執行結束,內部函數開始被調用
outer執行環境等待被回收,outer的做用域鏈對全局變量對象和outer的活動對象引用都斷了
像上面這種內部函數的做用域鏈仍然保持着對父函數活動對象的引用,就是閉包(closure)
閉包
閉包有兩個做用:
可是它也存在缺陷,可能產生內存泄漏(可是如今通常瀏覽器能夠解決這種問題)
關於第二點,舉個例子說明:
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){//注:i是outer()的局部變量
result[i] = function(){
return i;
}
}
return result;//返回一個函數對象數組
//這個時候會初始化result.length個關於內部函數的做用域鏈
}
var fn = outer();
console.log(fn[0]());//result:2
console.log(fn[1]());//result:2
</script>
複製代碼
返回結果很出乎意料吧,你確定覺得依次返回0,1,但事實並不是如此
來看一下調用fn0的做用域鏈圖:
能夠看到result[0]函數的活動對象裏並無定義i這個變量,因而沿着做用域鏈去找i變量,結果在父函數outer的活動對象裏找到變量i(值爲2),而這個變量i是父函數執行結束後將最終值保存在內存裏的結果。
由此也能夠得出,js函數內的變量值不是在編譯的時候就肯定的,而是等在運行時期再去尋找的。
那怎麼才能讓result數組函數返回咱們所指望的值呢?
看一下result的活動對象裏有一個arguments,arguments對象是一個參數的集合,是用來保存對象的。 那麼咱們就能夠把i當成參數傳進去,這樣一調用函數生成的活動對象內的arguments就有當前i的副本。
改進以後:
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個帶參函數
function arg(num){
return num;
}
//把i當成參數傳進去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]);//result:0
console.log(fn[1]);//result:1
</script>
複製代碼
雖然的到了指望的結果,可是又有人問這算閉包嗎?調用內部函數的時候,父函數的環境變量還沒被銷燬呢,並且result返回的是一個整型數組,而不是一個函數數組!
確實如此,那就讓arg(num)函數內部再定義一個內部函數就行了:
這樣result返回的實際上是innerarg()函數
<script>
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個帶參函數
function arg(num){
function innerarg(){
return num;
}
return innerarg;
}
//把i當成參數傳進去
result[i] = arg(i);
}
return result;
}
var fn = outer();
console.log(fn[0]());
console.log(fn[1]());
</script>
複製代碼
當調用outer,for循環內i=0時的做用域鏈圖以下:
由上圖可知,當調用innerarg()時,它會沿做用域鏈找到父函數arg()活動對象裏的arguments參數num=0.
上面代碼中,函數arg在outer函數內預先被調用執行了,對於這種方法,js有一種簡潔的寫法
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
//定義一個帶參函數
result[i] = function(num){
function innerarg(){
return num;
}
return innerarg;
}(i);//預先執行函數寫法
//把i當成參數傳進去
}
return result;
}
複製代碼