函數在javascript中扮演著一個重要的角色,範圍可以確定哪些變數可以被函數訪問,確定this的值,而且也關係到代碼的效能,所以理解函數的建立和執行過程及範圍至關重要。
首先得瞭解幾個名詞(其實有些名詞本人也不是很明白):
1.範圍(scope):在javascript沒有塊級範圍,是由函數來劃分的。變數和函數的範圍是在定義時決定而不是執行時決定,也就是說詞法範圍取決於源碼,通過靜態分析就能確定,因此詞法範圍也叫做靜態範圍(with和eval除外)。當定義了一個函數,當前的範圍鏈就儲存起來,並且成為函數的內部狀態的一部份。在最頂級範圍鏈僅由全域對象組成,而不和詞法範圍相關,然而,當定義一個嵌套的函數時,範圍鏈就包括外面的包含函數。這意味著嵌套函數可以訪問包含函數的所有參數和局部變數。儘管當一個函數定義時範圍鏈就固定了,但範圍鏈中定義的屬性還沒有固定。範圍鏈是活的,並且函數被調用時,可以訪問任何當前的綁定。
2.範圍鏈(scope chain):儲存變數對象的集合(環境棧?),保證對執行環境有權訪問的所有變數和函數的有序訪問,也就是用於標識符解析(變數訪問)。
3.執行環境(execution context):定義了變數和函數有權訪問的其他資料,決定了它們的各自行為。每個執行環境都有一個與之關聯的變數對象。
在web瀏覽器中,window對象就是全域執行環境。每個函數都有自己的執行環境,當函數執行完畢,該環境被銷毀,儲存在其中的變數和函數也隨之銷毀。
4.變數對象(variable object):儲存了環境中定義的所有變數和函數。該對象無法訪問,僅供解析器在後台使用。
5.使用中的物件(activation object):如果執行環境是函數,則將其使用中的物件作為變數對象(調用對象?)。使用中的物件最開始的兩個屬性是arguments和this
總的來說,以上所說的名詞都是js程式員不可直接操作的,瞭解它們可協助我們理解js引擎的在處理代碼的是如何工作的。
一、範圍鏈
a) 一個函數建立時,javascript後台(引擎)會預設建立一個僅供後台使用的內部屬性[[Scope]],此屬性儲存區函數的範圍鏈,如果是全域函數,此時則包含一個變數對象(全域變數),如果是嵌套函數(閉包),範圍鏈還加上了父函數的變數對象。例如下面的這個全域函數:
function add(num1,num2){ var sum = num1 + num2; return sum;}
(此圖是函數定義時的範圍鏈)
b) 函數被調用時--add(5,10),javascript後台會建立一個內部對象(execution context)--“執行環境”或“運行期上下文”,執行環境有它自己的範圍鏈,執行環境建立時就以定義函數時的範圍鏈初始化它自己的範圍鏈,並且隨後建立了一個使用中的物件,使用中的物件作為函數執行期的一個變數對象,包含所有局部變數(在函數內定義的)、具名引數、arguments、this,它會被推入到執行環境範圍鏈的前端(如)。每執行一次函數都會建立一個新的執行環境,當函數執行完畢執行環境就會被銷毀。
(此圖是函數運行時的範圍鏈)
另外關於延長範圍鏈問題:以下的兩種情況會使範圍鏈延長
1) try-catch語句的catch塊;
2) with語句;
這個兩個語句都會再原本的範圍鏈的前端添加一個變數對象。對於with語句來說,新添加的變數對象包含著with括弧中指定對象的所有屬性和方法所作的變數聲明。對於catch來說,當try塊發生錯誤時,代碼執行流程自動轉入到catch塊,並將異常對象推入到範圍鏈的前端。catch塊執行完畢後,範圍鏈就會返回原來的狀態。
請看下面的例子:
function initUI(){ with(document){ var bd = body, links = getElementsByTagName("a"), i = 0, len = links.length; while(i<len){ update(links[i++]); } getElementById("go-btn").onclick = function(){ start(); }; bd.className = "active" }}
當代碼流執行到一個with運算式時,執行環境的範圍鏈會被臨時改變,此時with的變數對象會被建立添加到範圍鏈的前端,這就意味著此時函數的所有局部變數都被推入到第二個範圍鏈中的變數對象,如:
由可清晰的看到,在執行with語句時,訪問局部變數的代價更高了。所以儘可能避免使用with語句,可以使用局部變數代替
var doc = document; // 代替with(document){...}
二、標識符解析/變數訪問
當在某個環境中為了讀取或寫入而引用一個標識符時,必須通過搜尋來確定該標識符實際代表什麼,搜尋過程始終從範圍鏈的前端開始,向上逐級查詢與給定名字匹配的標識符。如果在局部環境中找到了該標識符,搜尋過程就會停止,變數就緒。否則繼續向上級搜尋直到找到標識符為止(如果在全域環境都找不到標識符,則意味著該變數未聲明,通常會導致錯誤發生),通過下面的例子來理解一下標識符查詢過程:
var color = "blue";function getColor(){ var color = "red"; return color;}alert(getColor()); //red
當執行函數getColor()會引用變數color,為了確定變數color的值,將開始變數color的搜尋過程,通過前面所述,我們知道這個函數執行環境的範圍鏈包含兩個變數對象,一個是執行函數時本身的使用中的物件,另一個就是全域對象。在這個搜尋過程中,首先就搜尋getColor()的變數對象,如果存在一個局部的變數定義,則搜尋會自動停止,不在進入下一個變數對象。所以函數搜素變數color,返回"red"。
理解上面標識符的解析過程,很明顯知道,變數查詢是要付出代價的,訪問局部變數要比訪問全域變數更快,因為不用向上搜尋範圍鏈。所以,當函數需要重複引用一個變數時,最好在局部變數定義它,儘管它已經在全域環境中已經定義好了。
如果你完全理解了下面的幾個例子,那你就掌握了本文所述的知識:
例子1:
1 var name = "window"; 2 var obj ={ 3 name:"object", 4 getName:function(){ 5 return function(){ 6 return this.name; 7 } 8 }() 9 };10 alert(obj.getName()); // 輸出什麼?
例子2:
1 f = function(){return true;}; 2 g = function(){return false;}; 3 (function() { 4 //alert(g()); 5 if (g() && [] == ![]) { 6 f = function f() {return false;}; 7 function g() {return true;} 8 } 9 10 })();11 alert(f()); // true or false ?
例子3:
1 function createFunc(){ 2 var funcs = new Array(); 3 for(var i=0;i<10;i++){ 4 funcs[i] = function(num){ 5 return function(){return num;} 6 }(i); 7 } 8 return funcs; 9 }10 var aFuncs = createFunc();11 alert(aFuncs[1]());