理解做用域

做用域是什麼

一套用來儲存變量的設計良好的規則,而且以後能夠方便的找到這些變量。java

要理解做用域,先了解一下編譯原理

  1. 分詞/詞法分析(Tokenizing/Lexing)
    這個過程會將由字符組成的字符串分解成(對編程語言來講)有意義的代碼塊,這些代碼塊被稱爲詞法單元(token)。例如,考慮程序var a = 2;。這段程序一般會被分解成爲下面這些詞法單元:var、a、=、2 、; 空格是否會被看成詞法單元,取決於空格在這門語言中是否具備意義。編程

  2. 解析/語法分析(Parsing)
    這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明告終構的樹。這個樹被稱爲「抽象語法樹」(Abstract Syntax Tree,AST)。數組

  3. 代碼生成
    將AST轉換爲可執行代碼的過程稱被稱爲代碼生成。這個過程與語言、目標平臺等息息相關。拋開具體細節,簡單來講就是有某種方法能夠將var a = 2;的AST轉化爲一組機器指令,用來建立一個叫做a的變量(包括分配內存等),並將一個值儲存在a中。編程語言

理解做用域,編譯器,引擎的關係

  • 用var a = 2;來作例子函數

  • 首先,編譯器遇到var a,編譯器會詢問同一個做用域的集合中是否存在該變量。若是是,編譯器會忽略該聲明, 繼續編譯。不然它會要求做用域在當前做用域的集合中聲明一個新的變量,並命名爲a設計

  • 接下來,編譯器會爲引擎生成運行時所需的代碼,這些代碼被用來處理a = 2這個賦值操做。引擎運行時會首先詢問做用域,在當前的做用域集合中是否存在一個叫做a的變量。若是是,引擎就會使用這個變量;若是否,引擎會繼續查找該變量(從做用域鏈中)。若是引擎最終找到了a變量,就會將2賦值給它。不然引擎就會舉手示意並拋出一個異常!code

總結:變量的賦值操做會執行兩個動做,首先編譯器會在當前做用域中聲明一個變量(若是以前沒有聲明過),而後在運行時引擎會在做用域中查找該變量,若是可以找到就會對它賦值。token

關於LHS和RHS

在引擎查找變量a來判斷它是否聲明過,,可是引擎執行怎樣的查找,會影響最終的查找結果。在例子中,引擎會爲變量a進行LHS查詢。另一個查找的類型叫做RHS。
我打賭你必定能猜到「L」和「R」的含義,它們分別表明左側和右側。
什麼東西的左側和右側?是一個賦值操做的左側和右側。
換句話說,當變量出如今賦值操做的左側時進行LHS查詢,出如今右側時進行RHS查詢。ip

總的來講若是查找的目的是對變量進行賦值,那麼就會使用LHS查詢;若是目的是獲取變量的值,就會使用RHS查詢。內存

做用域嵌套

做用域嵌套當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。所以,在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量,或抵達最外層的做用(也就是全局做用域)爲止。
考慮如下代碼:

function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4

做用域共有兩種主要的工做模型。第一種是最爲廣泛的,被大多數編程語言所採用的詞法做用域。另一種叫做動態做用域,仍有一些編程語言在使用(好比Bash腳本、Perl中的一些模式等)。

詞法做用域

詞法做用域就是定義在詞法階段的做用域,換句話說,詞法做用域是由你寫在代碼時將變量和代碼塊做用域寫在哪裏來決定。
看下面代碼

function foo(a){
    var b = a*2;
    fucntion bar(c){
        console.log(a,b,c);
    }
    bar(b*3);
}
foo(2);//2,4,12
//這個例子中有三個助劑嵌套的做用域,其中有一個標識符:foo
//包含着foo所建立的做用域,其中有三個標識符:a, bar 和 b
//包含bar所建立的做用域,其中只有一個標識符:c

做用域查找會從運行時所在時所處的最內部做用域開始,逐級向上直到找到第一個匹配的標識符時中止。

不管函數在哪裏被調用,也不管如何被調用,它的詞法做用域都只由函數被聲明時所處的位置決定。
看下面代碼

var name = '小紅';
function showName() {
    console.info(name);
}

function show() {
    var name = '小黑';
    showName();
} 
show();//小紅

若是你記住而且理解了上面的話,那麼應該能夠獲得這個結果。用做用域鏈的角度解析:執行show()函數時,進入function show(){}的做用域內,而後執行showName()函數,再進入到function showName(){}的做用域內,要輸出name,就在當前做用域找,可是找不到,而後就向上爬一層,在全局環境中找到了var name = '小紅';,因此show()就輸出了小紅

函數中的做用域

函數做用域的含義是指,屬於這個函數的所有變量均可以在整個函數的範圍內使用及複用(事實上在嵌套的做用域中也可使用)

提高

不管做用域中的聲明出如今什麼地方,都將在代碼自己被執行前首先進行處理。能夠將這個過程形象地想象成全部的聲明(變量和函數)都會被「移動」到各自做用域的最頂端,這個過程被稱爲 提高
任何聲明在某個做用域內的變量,都將附屬於這個做用域。可是做用域同其中的變量聲明出現的位置有某種微妙的關係。

考慮一下代碼

a = 2
var a;
console.log(a);//2

說明先有聲明後有賦值
只有聲明自己纔會被提高,而賦值或其餘運行邏輯會留在原地。若是提高改變代碼的執行順序,會形成很是嚴重的破壞。
再看個例子

foo();
function(){
    console.log(a);//undefined
    var a = 2;
}
 //foo函數的聲明被提高了,所以函數第一行中的調用能夠正常執行,函數內部對var a進行提高。所以上面的代碼會被理解成下面的形式。

function foo(){
    var a;
    console.log(a);
    a = 2;
}
foo();

函數優先

函數聲明和變量聲明都會被提高。但首先是被提高,而後纔是變量。
考慮以下代碼

foo()
var foo;
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
};
//會輸出1而不是2,這個代碼片斷會被解釋成

function foo(){
    console.log(1);
}
foo();//1
foo = function(){
    console.log(2);
}

//出如今後面的函數聲明仍是能夠覆蓋前面的
foo();//3
function foo(){
    console.log(1);
}
var foo = function(){
    console.log(2);
}
function foo(){
    console.log(3);
}

最後要注意避免重複聲明,特別是當普通的var聲明和函數聲明混合在一塊兒的時候,不然會出現問題

相關文章
相關標籤/搜索