作用域
先來談?wù)勛兞康淖饔糜?/h5>
- 變量的作用域無非就是兩種:全局變量和局部變量
- 全局作用域:最外層函數(shù)定義的變量擁有全局作用域,即對任何內(nèi)部函數(shù)來說,都是可以訪問的,看例子:
var outVar = "全局變量";
function fn() {
console.log(outVar);
}
fn(); //"全局變量"
- 局部作用域:和全局作用域相反,一般只在固定的代碼片段內(nèi)可以訪問到,而對于函數(shù)外部是無法訪問的,最常見的例如函數(shù)內(nèi)部,看例子:
function fn(){
var innerVar = "inner";
}
fn();
console.log(innerVar);// ReferenceError: innerVar is not defined
var outVar = "全局變量";
function fn() {
console.log(outVar);
}
fn(); //"全局變量"
function fn(){
var innerVar = "inner";
}
fn();
console.log(innerVar);// ReferenceError: innerVar is not defined
需要注意的是,函數(shù)內(nèi)部聲明變量的時候,一定要使用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的一個特點,只要函數(shù)內(nèi)定義了一個局部變量,函數(shù)在解析的時候都會將這個變量“提前聲明”:
var scope = "global";
function fn(){
var scope;//提前聲明了局部變量
console.log(scope);//result:undefined
scope = "local";
console.log(scope);//result:local;
}
fn();
然而,也不能因此草率地將局部作用域定義為:用var聲明的變量作用范圍起止于花括號之間。
javascript并沒有塊級作用域
那什么是塊級作用域?
像在C/C++中,花括號內(nèi)中的每一段代碼都具有各自的作用域,而且變量在聲明它們的代碼段之外是不可見的,比如下面的c語言代碼:
for(int i = 0; i < 10; i++){
//i的作用范圍只在這個for循環(huán)
}
printf("%d",&i);//error
但是javascript不同,并沒有所謂的塊級作用域,javascript的作用域是相對函數(shù)而言的,可以稱為函數(shù)作用域:
for(var i = 1; i < 10; i++){
//coding
}
console.log(i); //10
作用域鏈(Scope Chain)
那什么是作用域鏈?
我的理解就是,根據(jù)在內(nèi)部函數(shù)可以訪問外部函數(shù)變量的這種機制,用鏈式查找決定哪些數(shù)據(jù)能被內(nèi)部函數(shù)訪問。
想要知道js怎么鏈式查找,就得先了解js的執(zhí)行環(huán)境
執(zhí)行環(huán)境(execution context):
- 是js中最為重要的概念,執(zhí)行環(huán)境定義了變量和函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了它們各自的行為。
- 執(zhí)行環(huán)境,總共有三種,全局環(huán)境,和局部環(huán)境(函數(shù)),eval
變量對象:
- 每個執(zhí)行環(huán)境,都有一個與之關(guān)聯(lián)的變量對象,執(zhí)行環(huán)境中定義的所有的變量和函數(shù)都保存在這個對象中
- 雖然我們編寫的代碼無法訪問這個對象,但是解析器在處理數(shù)據(jù)的時候會在回臺使用它。
全局執(zhí)行環(huán)境:
- 是最外圍的一個執(zhí)行環(huán)境,根據(jù)ECMAScript 實現(xiàn)的所在宿主環(huán)境不同,表示執(zhí)行環(huán)境的對象也不一樣
- 在 Web瀏覽器中,全局執(zhí)行環(huán)境,被認為是window對象,因此所有的全局變量和函數(shù),都是作為window對象的屬性和方法創(chuàng)建的;
- 某個執(zhí)行環(huán)境中的所有代碼執(zhí)行完畢后,該環(huán)境銷毀,保存在其中所有變量和函數(shù)定義也會銷毀
- 全局執(zhí)行環(huán)境直到應用退出(例如關(guān)閉網(wǎng)頁,或者瀏覽器時)才會被銷毀
環(huán)境棧:
- 每個函數(shù),都有自己的執(zhí)行環(huán)境,當執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境就會被推入一個環(huán)境棧中,而在函數(shù)執(zhí)行之后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境,ECMAScript 程序中的執(zhí)行流正是由這個方便的機制控著
作用域鏈:
- 作用域鏈本質(zhì)上,是一個指向變量對象的指針列表,它只引用,但不實際包含變量對象
- 當代碼在一個環(huán)境中執(zhí)行時,會創(chuàng)建變量對象的一個作用域鏈,作用域鏈的用途,是保證對執(zhí)行環(huán)境有權(quán)訪問的變量和函數(shù)的有序訪問,作用域鏈的前端,始終都是當前執(zhí)行的代碼所在環(huán)境的變量對象
- 如果這個環(huán)境是函數(shù),則其活動對象作為變量對象,活動對象,在最開始時只包含一個變量即argumengts對象(這個對象,在全局環(huán)境中是不存在的)作用域鏈中的下一個變量對象來自包含(外部)環(huán)境,而再下一個變量對象則來自于下一個包含環(huán)境,這樣,一直延伸到全局執(zhí)行環(huán)境;
4.全局執(zhí)行環(huán)境的變量對象,始終都是作用域鏈中的最后一個對象
標識符的解析:
- 是沿著作用域鏈一級一級的搜索標識符的過程,搜索過程始終從作用域鏈的前端開始,然后逐級地向后回溯,直至找到標識符為止(如果找不到標識符,通常會導致錯誤發(fā)生)
注意:內(nèi)部環(huán)境,可以通過作用域鏈訪問所有的外部環(huán)境,但是外部環(huán)境不能訪問內(nèi)部環(huán)境中的任何變量和函數(shù),這些環(huán)境之間的 聯(lián)系,是線性,有次序的,每個環(huán)境都可以向上搜索作用域鏈,以查詢變量和函數(shù)名,但是任何環(huán)境都不能通過向下搜索作用域鏈而進入另一個執(zhí)行環(huán)境
舉個例子:
<script>
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
</script>
上面代碼執(zhí)行情況演示:

了解了環(huán)境變量,再詳細講講作用域鏈。
當某個函數(shù)第一次被調(diào)用時,就會創(chuàng)建一個執(zhí)行環(huán)境(execution context)以及相應的作用域鏈,并把作用域鏈賦值給一個特殊的內(nèi)部屬性([scope])。然后使用this,arguments(arguments在全局環(huán)境中不存在)和其他命名參數(shù)的值來初始化函數(shù)的活動對象(activation object)。當前執(zhí)行環(huán)境的變量對象始終在作用域鏈的第0位。
以上面的代碼為例,當?shù)谝淮握{(diào)用fn1()時的作用域鏈如下圖所示:
(因為fn2()還沒有被調(diào)用,所以沒有fn2的執(zhí)行環(huán)境)

可以看到fn1活動對象里并沒有scope變量,于是沿著作用域鏈(scope chain)向后尋找,結(jié)果在全局變量對象里找到了scope,所以就返回全局變量對象里的scope值。
那作用域鏈地作用僅僅只是為了搜索標識符嗎?
再來看一段代碼:
<script>
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn();
</script>
outer()內(nèi)部返回了一個inner函數(shù),當調(diào)用outer時,inner函數(shù)的作用域鏈就已經(jīng)被初始化了(復制父函數(shù)的作用域鏈,再在前端插入自己的活動對象),具體如下圖:

一般來說,當某個環(huán)境中的所有代碼執(zhí)行完畢后,該環(huán)境被銷毀(彈出環(huán)境棧),保存在其中的所有變量和函數(shù)也隨之銷毀(全局執(zhí)行環(huán)境變量直到應用程序退出,如網(wǎng)頁關(guān)閉才會被銷毀)
但是像上面那種有內(nèi)部函數(shù)的又有所不同,當outer()函數(shù)執(zhí)行結(jié)束,執(zhí)行環(huán)境被銷毀,但是其關(guān)聯(lián)的活動對象并沒有隨之銷毀,而是一直存在于內(nèi)存中,因為該活動對象被其內(nèi)部函數(shù)的作用域鏈所引用。
具體如下圖:
outer執(zhí)行結(jié)束,內(nèi)部函數(shù)開始被調(diào)用
outer執(zhí)行環(huán)境等待被回收,outer的作用域鏈對全局變量對象和outer的活動對象引用都斷了

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