關於JS裏的函數做用域鏈的總結

  在JavaScript中,函數的做用域鏈是一個很難理解的東西。這是由於JavaScript中函數的做用域鏈和其餘語言好比C、C++中函數的做用域鏈相差甚遠。本文詳細解釋了JavaScript中與函數的做用域鏈相關的知識,理解這些知識能夠幫助你在處理閉包的時候避免一些可能出現的問題。前端

  在JavaScript中,函數可讓你在一次調用中執行一系列的操做。有多種方式來定義一個函數,以下:閉包

一、函數聲明:函數

function maximum(x, y) { if (x > y) return x; else return y; } maximum(5, 6) //返回6;

  這種語法一般用來定義全局做用域下的函數(全局函數)this

二、函數表達式:spa

var obj = new Object(); obj.maximum = function (x, y) { if (x > y) return x; else return y; }; obj.maximum(5, 6) //返回6;

  這種語法一般用來定義一個做爲對象方法的函數code

三、Function構造函數:對象

var maximum = new Function("x", "y", "if(x > y) return x; else return y;"); maximum(5, 6); //返回6;

  以這種形式定義函數一般沒有很好的可讀性(沒有縮進),只在特定狀況下使用。blog

函數定義:

  函數定義指的是在JavaScript引擎內部建立一個函數對象的過程。ip

  若是是全局函數的話,這個函數對象會做爲屬性添加到全局對象上;內存

  若是是內部函數(嵌套函數)的話,該函數對象會做爲屬性添加到上層函數的活動對象上,屬性名就是函數名。須要指出的是,若是函數是以函數聲明的方式定義的,則函數的定義操做會發生在腳本解析的時候。

  以下例中當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 。這是怎麼回事?"。

  答案就是在定義func1和func2時,函數outerFn中產生過兩個不一樣的活動對象。

  如今又出現了一個問題, 一個活動對象在函數執行的時候建立,但在函數執行完畢返回的時候不會被銷燬嗎? 我用下面的三個例子來說解這個問題。

(1)沒有內部函數的函數

function outerFn(x) { return x * x; } var y = outerFn(2);

  若是函數沒有內部函數,則在該函數執行時,當前活動對象會被添加到該函數的做用域鏈的最前端。

  做用域鏈是惟一引用這個活動對象的地方。當函數退出時,活動對象會被從做用域鏈上刪除,因爲再沒有任何地方引用這個活動對象,則它隨後會被垃圾回收器銷燬。

(2)包含內部函數的函數,但這個內部函數沒有被外部函數以外的變量所引用

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函數的活動對象。但因爲它們都沒有外部引用,因此都將會被垃圾回收器回收。

(3) 包含內部函數的函數,但外部函數以外存在指向這個內部函數的引用(閉包)

  2種狀況:

function outerFn(x) { //內部函數做爲outerFn的返回值被引用到了外部
    return function innerFn() { return x * x; } } //引用着返回的內部函數
var square = outerFn(5); square();
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。這樣的循環引用就可能形成內存泄漏。

相關文章
相關標籤/搜索