作用域 (1)、作用域也叫執行環境(execution context)是JavaScript中一個重要的概念。執行環境定義了變數或函數有權訪問的其他數據,決定了它們各自的行為。在JavaScript中變數的作用域有全局作用域和局部作用域,全局變數是指變數沒有在函數體內聲明或者在函數內聲明的時候沒有 ...
作用域
(1)、作用域也叫執行環境(execution context)是JavaScript中一個重要的概念。執行環境定義了變數或函數有權訪問的其他數據,決定了它們各自的行為。在JavaScript中變數的作用域有全局作用域和局部作用域,全局變數是指變數沒有在函數體內聲明或者在函數內聲明的時候沒有帶var,即表示擁有全局作用域,相反變數在函數內聲明帶var稱為局部變數,擁有局部作用域。特殊的雖然函數參數不帶var但它也屬於局部變數。
1 var a = 1; //全局變數 2 function fn1( b ){ //局部變數 3 var c = 2; //局部變數 4 d = 3; //全局變數 5 }
(2)、接下來介紹一下解析過程。瀏覽器有專門的一段程式用於解析JavaScript,暫且給他取個名字叫JavaScript解析工具,這個解析過程至少有兩個過程(當然不僅僅有兩個,像編譯原理的詞法分析什麼的,這裡不提):1.尋找目標(預解析)。包括var、function、參數等。 2.逐行解讀代碼。接下來通過實例來具體分析是怎麼做的:
1 alert( a ); //undefined 2 var a = 1; 3 function fn (){ 4 alert( 2 ); 5 }
具體過程:
1.尋找目標。包括var、function、參數等 a = undefined (所有的變數在正式運行代碼之前,都會提前賦值一個值,即undefined)。 fn = function() { alert(2) } (所有函數在正式運行之前,都是整個函數塊) 2.逐行解讀代碼。 解讀代碼時表達式會改變預解析中的值,如以上代碼解讀到第二行時,a = undefined 變為了 a = 1; 特殊的:如果預解析過程中遇到重名的,只留一個。如變數和函數重名了,就只留函數。再來一個例子說明:1 alert( b ); //function a() { alert( 4 ) }; 2 var b = 1; 3 alert( b ); //1 4 function b () { 5 alert( 2 ); 6 } 7 alert( b ); //1 8 var b = 3; 9 alert( b ); //3 10 function b (){ 11 alert( 4 ); 12 } 13 alert( b ); //3解析過程還是那兩步,只是多了重名的情況。1.預解析後只留下 b = function b(){ alert(4) }。2.逐行解讀後,預解析中a的值變為3,若在代碼的最後調用 b(), 則會在控制台報錯。 (3)JavaScript中沒有塊級作用域。先看一個例子:
1 if( true ){ 2 var a = 1; 3 } 4 alert( a ); //1
在一個 if 語句中定義變數 a。如果是在 C、C++等語言中,a會在 if 語句執行完畢後被銷毀。但在JavaScript中,if 語句中的變數聲明會將變數添加到當前的作用域(在這裡是全局作用域)中。特別是在使用 for 語句時:
1 for( var i=0; i<10; i++ ){ 2 doSothing(i); 3 } 4 alert(i); //10
對於塊級作用域的語言來說,for語句初始化變數的表達式所定義的變數,只會存在於迴圈的環境中。而對於JavaScript來說,有for語句創建的變數 i 即使在for迴圈執行結束後,也依舊會存在於迴圈外部的作用域中。
(4)、函數是作用域,有預解析等過程,if for語句不是作用域。儘量不要在 if for中定義變數和函數調用,否則有瀏覽器問題:
1 alert(fn1); //chrome,FF 彈出 undefine. ie 彈出整段函數(function fn19=(){alert( 123 )}) 2 if( true ){ 3 var a = 1; 4 function fn1(){ 5 alert( 123 ) 6 } 7 }
作用域鏈 1.當代碼在一個環境中執行時,會創建變數對象的一個作用域鏈。它的作用是保證對執行環境有權訪問所有變數和函數的有序訪問。也就是說作用域鏈就像是一種繩索可以將各個作用域連接起來,已達到可以訪問各個域中的變數,當然這種訪問是要遵守一定的規則的,即局部作用域可以通過作用域鏈訪問所有的全局作用域,但是全局作用域不能訪問局部環境中的任何變數或函數。看下麵的例子:
1 var a = 1; 2 function change{ 3 var b = 2; 4 function swap(){ 5 var c = b; 6 b = a; 7 a = c; 8 9 //這裡可以訪問a,b,c 10 } 11 12 //這裡可以訪問a和b,但不能訪問c 13 swap(); 14 } 15 16 //這裡只能訪問a 17 change();
通過上面例子我們知道作用域之間的 聯繫是線性、有次序的。每個作用域可以沿著作用域鏈向上搜索,但任何作用域不能通過向下搜索而進入另一個作用域中,搜索時先搜索自己作用域內是否存在該變數,若不存在則再一級一級往上搜索。
2.既然我們不能通過這種方式去訪問局部作用域,那我們也可以用以下方法去獲取函數內部的值:
(1)、通過設置全局變數獲取
1 var str = ''; 2 function fn1(){ 3 var a = '需要拿到的值'; 4 str = a; 5 } 6 fn1(); 7 alert( str ); //彈出'需要拿到的值'
(2)、通過函數調用獲取
1 function fn2(){ 2 var a = '需要拿到的值'; 3 fn3( a ); 4 } 5 fn2(); 6 function fn3( a ){ 7 alert( a ); 8 }
作用域鏈的改變
JavaScript里的 with語句和 catch語句可以在作用域的頭部臨時增加一個變數對象,該變數對象會在代碼執行後被移除,具體來說就是當執行到這兩個語句時,作用域鏈會得到加長。
(1)、with語句的作用是避免重覆書寫代碼,如:
1 function fn(){ 2 with(document){ 3 var btn = getElementById('btn'); 4 var input = getElementsByClassName('input'); 5 } 6 }
這裡with語句接收一個document對象,因此它的變數對象中就包含了document對象的所有屬性和方法,但這個變數對象就被添加到作用域的最前頭,這樣看似避免了重覆書寫,但是性能並不好。因為被推到作用域前頭,其他的變數就處於第二個作用域當中了,若要逐級訪問,訪問代價比較大。可以用一局部變數代替document,即可解決,而不必用with語句。
(2)catch語句與with相類似:
1 try { 2 //可能出錯的代碼 3 } catch (error) { 4 //出錯時怎麼處理 5 }
當出現錯誤執行catch語句,將出錯對象放入作用域頭部,然後catch中其他的變數就處於第二個作用域當中了。
最後通過幾個例子強化一下,每個例子都是在前一個例子的基礎上做一些調整,但結果卻不一樣。
(1)、
1 var a = 1; 2 function fn1(){ 3 alert(a); //undefined 4 var a = 2; 5 } 6 fn1(); 7 alert(a); //1
(2)、將(1)的第四行改為 a = 2。
1 var a = 1; 2 function fn1(){ 3 alert(a); //1 4 a = 2; 5 } 6 fn1(); 7 alert(a); //2
(3)、將(2)中的 fn1函數添加參數 a,雖然結果與(1)相同,但解析過程不同。
1 var a = 1; 2 function fn1( a ){ //a相當於局部變數,相當於var a 3 alert(a); //undefined 4 a = 2; 5 } 6 fn1(); 7 alert(a); //1
(4)、在(3)的基礎上給第6行添加參數 a。
1 var a = 1; 2 function fn1( a ){ //a相當於局部變數,相當於var a 3 alert(a); //1 4 a = 2; 5 } 6 fn1(a); 7 alert(a); //1
有錯誤的地方請指正。
參考資料:《JavaScript高級程式設計》