深入理解JavaScript中的作用域、作用域鏈

作用域

先來談談變量的作用域
變量的作用域無非就是兩種:全局變量和局部變量。

全局作用域:

最外層函數定義的變量擁有全局作用域,即對任何內部函數來說,都是可以訪問的:

var outerVar = "outer";
function fn(){
 console.log(outerVar);
}
fn();//result:outer

局部作用域:

和全局作用域相反,局部作用域一般只在固定的代碼片段內可訪問到,而對於函數外部是無法訪問的,最常見的例如函數內部

 

function fn(){
 var innerVar = "inner";
}
fn();
console.log(innerVar);// ReferenceError: innerVar is not defined

需要注意的是,函數內部聲明變量的時候,一定要使用var命令。如果不用的話,你實際上聲明瞭一個全局變量!

   

function fn(){
 innerVar = "inner";
}
fn();
console.log(innerVar);// result:inner

再來看一個代碼:

var scope = "global";
function fn(){
 console.log(scope);//result:undefined
 var scope = "local";
 console.log(scope);//result:local;
}
fn();

很有趣吧,第一個輸出居然是undefined,原本以爲它會訪問外部的全局變量(scope=”global”),但是並沒有。這可以算是javascript的一個特點,只要函數內定義了一個局部變量,函數在解析的時候都會將這個變量“提前聲明”:

var scope = "global";
function fn(){
 var scope;//提前聲明瞭局部變量
 console.log(scope);//result:undefined
 scope = "local";
 console.log(scope);//result:local;
}
fn();

然而,也不能因此草率地將局部作用域定義爲:用var聲明的變量作用範圍起止於花括號之間。
javascript並沒有塊級作用域
那什麼是塊級作用域?

像在C/C++中,花括號內中的每一段代碼都具有各自的作用域,而且變量在聲明它們的代碼段之外是不可見的,比如下面的c語言代碼:

   

 for(int i = 0; i < 10; i++){
    //i的作用範圍只在這個for循環
    }
    printf("%d",&i);//error

但是javascript不同,並沒有所謂的塊級作用域,javascript的作用域是相對函數而言的,可以稱爲函數作用域:

for(var i = 1; i < 10; i++){
	//coding
}
console.log(i); //10 

作用域鏈(Scope Chain)

那什麼是作用域鏈?
我的理解就是,根據在內部函數可以訪問外部函數變量的這種機制,用鏈式查找決定哪些數據能被內部函數訪問。
想要知道js怎麼鏈式查找,就得先了解js的執行環境

執行環境(execution context)

每個函數運行時都會產生一個執行環境,而這個執行環境怎麼表示呢?js爲每一個執行環境關聯了一個變量對象。環境中定義的所有變量和函數都保存在這個對象中。
全局執行環境是最外圍的執行環境,全局執行環境被認爲是window對象,因此所有的全局變量和函數都作爲window對象的屬性和方法創建的。
js的執行順序是根據函數的調用來決定的,當一個函數被調用時,該函數環境的變量對象就被壓入一個環境棧中。而在函數執行之後,棧將該函數的變量對象彈出,把控制權交給之前的執行環境變量對象。

舉個例子:

var scope = "global";
function fn1(){
 return scope;
}
function fn2(){
 return scope;
}
fn1();
fn2();

上面代碼執行情況演示:

瞭解了環境變量,再詳細講講作用域鏈。
當某個函數第一次被調用時,就會創建一個執行環境(execution context)以及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性([scope])。然後使用this,arguments(arguments在全局環境中不存在)和其他命名參數的值來初始化函數的活動對象(activation object)。當前執行環境的變量對象始終在作用域鏈的第0位。
以上面的代碼爲例,當第一次調用fn1()時的作用域鏈如下圖所示:
(因爲fn2()還沒有被調用,所以沒有fn2的執行環境)


 

可以看到fn1活動對象裏並沒有scope變量,於是沿着作用域鏈(scope chain)向後尋找,結果在全局變量對象裏找到了scope,所以就返回全局變量對象裏的scope值。

    標識符解析是沿着作用域鏈一級一級地搜索標識符地過程。搜索過程始終從作用域鏈地前端開始,然後逐級向後回溯,直到找到標識符爲止(如果找不到標識符,通常會導致錯誤發生)—-《JavaScript高級程序設計》

那作用域鏈地作用僅僅只是爲了搜索標識符嗎?
再來看一段代碼:

function outer(){
 var scope = "outer";
 function inner(){
	return scope;
 }
 return inner;
}
var fn = outer();
fn();

outer()內部返回了一個inner函數,當調用outer時,inner函數的作用域鏈就已經被初始化了(複製父函數的作用域鏈,再在前端插入自己的活動對象),具體如下圖:



一般來說,當某個環境中的所有代碼執行完畢後,該環境被銷燬(彈出環境棧),保存在其中的所有變量和函數也隨之銷燬(全局執行環境變量直到應用程序退出,如網頁關閉纔會被銷燬)
但是像上面那種有內部函數的又有所不同,當outer()函數執行結束,執行環境被銷燬,但是其關聯的活動對象並沒有隨之銷燬,而是一直存在於內存中,因爲該活動對象被其內部函數的作用域鏈所引用。
具體如下圖:
outer執行結束,內部函數開始被調用
outer執行環境等待被回收,outer的作用域鏈對全局變量對象和outer的活動對象引用都斷了


像上面這種內部函數的作用域鏈仍然保持着對父函數活動對象的引用,就是閉包(closure)

 

詳見閉包可戳這裏~

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章