原文:http://blogs.msdn.com/b/jscript/archive/2007/07/26/scope-chain-of-jscript-functions.aspx
在JavaScript中,函數的作用域鏈是一個很難理解的東西.這是因為,JavaScript中函數的作用域鏈和其他語言比如C, C++中函數的作用域鏈相差甚遠.本文詳細解釋了JavaScript中與函數的作用域鏈相關的知識,理解這些知識可以幫助你在處理閉包的時候避免一些可能出現的問題.
在JavaScript中,函數可以讓你在一次調用中執行一系列的操作.有多種方式來定義一個函數,如下:
函數聲明:
function maximum(x, y) { if (x > y) return x; else return y; } maximum(5, 6) //返回6;
這種語法通常用來定義全局作用域下的函數(全局函數).
函數表達式:
var obj = new Object(); obj.maximum = function (x, y) { if (x > y) return x; else return y; };
obj.maximum(5, 6) //返回6;
這種語法通常用來定義一個作為對象方法的函數.
Function構造函數:
var maximum = new Function("x", "y", "if(x > y) return x; else return y;"); maximum(5, 6); //返回6;
以這種形式定義函數通常沒有很好的可讀性(沒有縮進),只在特定情況下使用.
函數定義:
函數定義指的是在JavaScript引擎內部創建一個函數對象的過程.如果是全局函數的話,這個函數對象會作為屬性添加到全局對象上,如果是內部函數(嵌套函數)的話,該函數對象會作為屬性添加到上層函數的活動對象上,屬性名就是函數名.需要指出的是,如果函數是以函數聲明的方式定義的,則函數的定義操作會發生在腳本解析的時候.如下例中,當JavaScript引擎完成腳本解析時,就已經創建了一個函數對象func,該函數對象作為屬性添加到了全局對象中,屬性名為"func".
/*func函數可以被訪問到,因為在腳本開始執行前func函數就已經存在了.*/ alert(func(2)); //返回8
//執行該語句會覆蓋func的值為true. var func = true;
alert(func); //返回"true";
/*在腳本開始執行前,解析下面的語句就會定義一個函數對象func.*/ function func(x) { return x * x * x; }
在下面的例子中,存在內部函數的情況.內部函數innerFn的定義操作發生在外部函數outerFn執行的時候(其實也是發生在執行前的解析階段),同時,內部函數會作為屬性添加到外部函數的活動對象上.
function outerFn() { function innerFn() {} } outerFn(); //執行outerFn函數的時候會定義一個函數innerFn
注: 對於使用Function構造函數定義的函數來說,函數定義操作就發生在執行Function構造函數的時候.
作用域鏈:
函數的作用域鏈是由一系列對象(函數的活動對象+0個到多個的上層函數的活動對象+最后的全局對象)組成的,在函數執行的時候,會按照先后順序從這些對象的屬性中尋找函數體中用到的標識符的值(標識符解析).函數會在定義時將它們各自所處環境(全局上下文或者函數上下文)的作用域鏈存儲到自身的[[scope]]內部屬性中. 首先看一個內部函數的例子:
function outerFn(i) { return function innerFn() { return i; } } var innerFn = outerFn(4); innerFn(); //返回4
當innerFn函數執行時,成功返回了變量i的值4,但變量i既不存在於innerFn函數自身的局部變量中,也不存在於全局作用域中.那么變量i的值是從哪兒得到的? 你也許認為內部函數innerFn的作用域鏈是由innerFn函數的活動對象+全局對象組成的.但這是不對的,只有全局函數的作用域鏈包含兩個對象,這並不適用於內部函數.讓我們先分析全局函數,然后再分析內部函數.
全局函數:
全局函數的作用域鏈很好理解.
var x = 10; var y = 0; function testFn(i) { var x = true; y = y + 1; alert(i); } testFn(10);
全局對象: JavaScript引擎在腳本開始執行之前就會創建全局對象,並添加到一些預定義的屬性"Infinity", "Math"等.在腳本中定義的全局變量也會成為全局對象的屬性.
活動對象: 當JavaScript引擎調用一些函數時,該函數會創建一個新的活動對象,所有在函數內部定義的局部變量以及傳入函數的命名參數和arguments對象都會作為這個活動對象的屬性.這個活動對象加上該函數的[[scope]]內部屬性中存儲的作用域鏈就組成了本次函數調用的作用域鏈.
內部函數:
讓我們分析一下下面的JavaScript代碼.
function outerFn(i, j) { var x = i + j; return function innerFn(x) { return i + x; } } var func1 = outerFn(5, 6); var func2 = outerFn(10, 20); alert(func1(10)); //返回15 alert(func2(10)); //返回20
在調用func1(10)和func2(10)時,你引用到了兩個不同的i .這是怎么回事?首先看下面的語句,
var func1 = outerFn(5,6);
調用outerFn (5, 6)的時候定義了一個新的函數對象innerFn,然后該函數對象成為了outerFn函數的活動對象的一個屬性.這時innerFn的作用域鏈是由outerFn的活動對象和全局對象組成的. 這個作用域鏈存儲在了innerFn函數的內部屬性[[scope]]中,然后返回了該函數,變量func1就指向了這個innerFn函數.
alert(func1(10));//返回15
在func1被調用時,它自身的活動對象被創建,然后添加到了[[scope]]中存儲着的作用域鏈的最前方(新的作用域鏈,並不會改變[[scope]]中存儲着的那個作用域鏈).這時的作用域鏈才是func1函數執行時用到的作用域鏈.從這個作用域鏈中,你可以看到變量‘i’的值實際上就是在執行outerFn(5,6)時產生的活動對象的屬性i的值.下圖顯示了整個流程.
現在讓我們回到問題,"在調用func1(10)和func2(10)時,你引用到了兩個不同的i .這是怎么回事?".讓我們從下圖中看一下func2執行時的情況,答案就是在定義func1和func2時,函數outerFn中產生過兩個不同的活動對象.
現在又出現了一個問題, 一個活動對象在函數執行的時候創建,但在函數執行完畢返回的時候不會被銷毀嗎? 我用下面的三個例子來講解這個問題.
i) 沒有內部函數的函數
function outerFn(x) { return x * x; } var y = outerFn(2);
如果函數沒有內部函數,則在該函數執行時,當前活動對象會被添加到該函數的作用域鏈的最前端.作用域鏈是唯一引用這個活動對象的地方.當函數退出時,活動對象會被從作用域鏈上刪除,由於再沒有任何地方引用這個活動對象,則它隨后會被垃圾回收器銷毀.
ii) 包含內部函數的函數,但這個內部函數沒有被外部函數之外的變量所引用
function outerFn(x) { //在outerFn外部沒有指向square的引用 function square(x) { return x * x; } //在outerFn外部沒有指向cube的引用 function cube(x) { return x * x * x; } var temp = square(x); return temp / 2; } var y = outerFn(5);
在這種情況下,函數執行時創建的活動對象不僅添加到了當前函數的作用域鏈的前端,而且還添加到了內部函數的作用域鏈中.當該函數退出時,活動對象會從當前函數的作用域鏈中刪除,活動對象和內部函數互相引用着對方,outerFn函數的活動對象引用着嵌套的函數對象square和cube,內部函數對象square和cube的作用域鏈中引用了outerFn函數的活動對象.但由於它們都沒有外部引用,所以都將會被垃圾回收器回收.
iii) 包含內部函數的函數,但外部函數之外存在指向這個內部函數的引用
例1:
function outerFn(x) { //內部函數作為outerFn的返回值被引用到了外部 return function innerFn() { return x * x; } } //引用着返回的內部函數 var square = outerFn(5); square();
例2:
var square; function outerFn(x) { //通過全局變量引用到了內部函數 square = function innerFn() { return x * x; } } outerFn(5); square();
在這種情況下,outerFn函數執行時創建的活動對象不僅添加到了當前函數的作用域鏈的前端,而且還添加到了內部函數innerFn的作用域鏈中(innerFn的[[scope]]內部屬性).當外部函數outerFn退出時,雖然它的活動對象從當前作用域鏈中刪除了,但內部函數innerFn的作用域鏈仍然引用着它. 由於內部函數innerFn存在一個外部引用square,且內部函數innerFn的作用域鏈仍然引用着外部函數outerFn的活動對象,所以在調用innerFn時,仍然可以訪問到outerFn的活動對象上存儲着的變量x的值.
多個內部函數:
更有趣的場景是有不止一個的內部函數,多個內部函數的作用域鏈引用着同一個外部函數的活動對象.該活動對象的改變會反應到三個內部函數上.
function createCounter(i) { function increment() { ++i; } function decrement() { --i; } function getValue() { return i; } function Counter(increment, decrement, getValue) { this.increment = increment; this.decrement = decrement; this.getValue = getValue; } return new Counter(increment, decrement, getValue); } var counter = createCounter(5); counter.increment(); alert(counter.getValue()); //返回6
上圖表示了createCounter函數的活動對象被三個內部函數的作用域鏈所共享.
閉包以及循環引用:
上面討論了JavaScript中函數的作用域鏈,下面談一下在閉包中可能出現因循環引用而產生內存泄漏的問題.閉包通常指得是能夠在外部函數外面被調用的內部函數.下面給出一個例子:
function outerFn(x) { x.func = function innerFn() {} } var div = document.createElement("DIV"); outerFn(div);
在上例中,一個DOM對象和一個JavaScript對象之間就存在着循環引用. DOM 對象div通過屬性‘func’引用着內部函數innerFn.內部函數innerFn的作用域鏈(存儲在內部屬性[[scope]]上)上的活動對象的屬性‘x’ 引用着DOM對象div. 這樣的循環引用就可能造成內存泄漏.
譯者注:猜測作者是為了使文章更易懂,故意不提及執行上下文的概念,本文中出現[[scope]]內部屬性的地方也是我加的.