8.8. 函數作用域與閉包
??????? 如第四章所述,JavaScript函數的函數體在局部作用域中執(zhí)行,局部作用域不同于全局作用域.本章將解釋這些內容和相關的作用域問題,包括閉包.[*]
[*] 本章包含超前的內容,如果你是第一次閱讀,可以跳過.
8.8.1. 詞法作用域(Lexical Scoping)
??????? JavaScript中的函數是基于詞法作用域的,而不是動態(tài)作用域.這句話的意思是JavaScript中的函數運行在它們被定義的作用域里,而不是它們被執(zhí)行的作用域里.定義一個函數時,當前作用域鏈被保存起來并成為該函數內部狀態(tài)的一部分.作用域鏈的頂層(最初一層)是由全局對象構成的,這和詞法作用域沒什么明顯的關聯.然而,當你定義一個嵌套函數時,作用域鏈將包含外層函數(嵌套函數的外層函數.原文:the containing function).這就意味著,被嵌套的函數可以訪問外層函數的所有參數和局部變量.
??????? 注意:盡管在一個函數定義的時候,作用域鏈就已經固定了,但是作用域鏈中定義的屬性并不是固定的.作用域鏈是"活的"("live"),當函數被調用的時候,它有權訪問任何當前被關聯的數據.
8.8.2. 調用對象(The Call Object)
??????? 當JavaScript解釋器調用函數的時候,首先,它把作用域設置到作用域鏈,在函數被定義的時候,該作用域鏈已經有效.接下來,解釋器添加一個叫做調用對象(ECMAScript規(guī)范使用術語:activation object,活動對象)的對象到作用域鏈的頭部.引用Arguments對象的arguments屬性為函數初始化調用對象.接下來,添加函數的命名參數到調用對象.所有用var語句定義的局部變量也都在這個對象中定義.因為調用對象在作用域鏈的頭部,局部變量,函數參數和參數對象都在函數的作用域內.也就是說它們隱藏了所有同名的在更早的作用域中定義的屬性.
注意:與arguments不同,this是關鍵字,而不是調用對象的一個屬性.
8.8.3. 調用對象作為命名空間(The Call Object as a Namespace)
??????? 有時,用定義一個簡單函數的方法創(chuàng)建一個調用對象是很有用的,這個調用對象可以扮演一個臨時命名空間的角色,如此一來,你定義的變量和創(chuàng)建的屬性都不會破壞全局命名空間.例如:假設你有一個Javascrip代碼文件,你希望把它用到很多不同的Javascript程序中(或者,用于客戶端Javascript,在很多不同的web頁上).假設這些代碼像其它代碼一樣定義了中間變量來保存計算結果.現在的問題是因為這些代碼將用于很多不同的程序,你無法知道此變量是否和其它引入該文件的程序的變量相沖突.
??????? 解決的方法是把代碼放到函數里,然后調用這個函數.如此一來,變量是被定義在函數的調用對象中:
function
?init()?
{
????
//
?代碼從這里開始
????
//
?任何變量聲明都會成為調用對象的屬性
????
//
?如此不會破壞全局命名空間.
}
init();??
//
?不要忘了調用這個函數哦!
??????? 這段代碼只給全局命名空間添加了一個"init"屬性,該屬性引用init函數.如果定義一個函數還嫌太多,那么你可以用一個表達式定義和調用一個匿名函數.像這樣的JavaScript語法如下:
(
function
()?
{??
//
?這個函數沒有名字.
????
//
?代碼從這里開始
????
//
?任何變量聲明都會成為調用對象的屬性
????
//
?如此不會破壞全局命名空間.
}
)();??????????
//
結束函數直接量,并調用該函數.
??????? 注意:函數直接量外面的括號是JavaScript語法所必需的.
8.8.4. 嵌套函數作為閉包(Nested Functions as Closures)
??????? JavaScript允許函數嵌套,允許把函數作為數據,允許使用詞法作用域,把這些結合使用能創(chuàng)造出功能強大的令人驚奇的效果.讓我們開始探索,考慮一下函數g被定義在函數f中.當f被調用的時候,作用域鏈由為函數f調用生成的調用對象跟隨在全局對象之后構成.g函數被定義在f函數里,因此,這個作用域鏈作為g函數定義的一部分被保存起來.當g函數被調用的時候,作用域鏈包括三個部分:g函數自己的調用對象,f函數的調用對象和全局對象.
??????? 嵌套函數在相同的它們被定義的詞法作用域里被調用的時候是很容易理解的.例如,下面的代碼并沒有什么特別:
var
?x?
=
?
"
global
"
;

function
?f()?
{
????
var
?x?
=
?
"
local
"
;

????
function
?g()?
{?alert(x);?}
????g();
}
f();??
//
?調用這個函數顯示?"local"
??????? 然而,在JavaScript中,函數可以像其它值一樣作為數據,因此可以在函數中返回一個函數,賦值給對象的屬性,存儲在數組中等等.這也沒有什么特別的,除了嵌套的函數被調用的時候.考慮下面的代碼,它包含一個返回嵌套函數的函數.每次被調用的時候,它都返回一個函數.被返回的函數的JavaScript代碼總是相同的,但是,因為每次調用外層函數時的參數不同,每次被調用的時候,它(被返回的嵌套函數)創(chuàng)建的作用域也有些許不同.(也就是說,對于外層函數的每次調用,都會在作用域鏈中產生一個不同的調用對象.)如果你把返回函數保存在數組中,然后每一個調用一次,你將發(fā)現每一個函數都返回不同的值.因為每一個函數都由相同的JavaScript代碼構成,并且每一次都是從相同的作用域中調用,所以,唯一能造成返回值不同的因素就是函數被定義的作用域:
//
?每次調用這個函數的時候返回一個函數
//
?函數被定義的作用域在每次調用時都不同
function
?makefunc(x)?
{

????
return
?
function
()?
{?
return
?x;?}
}
//
?調用幾次?makefunc()?,?把結果保存到數組中:
var
?a?
=
?[makefunc(
0
),?makefunc(
1
),?makefunc(
2
)];

//
?現在調用這些函數并顯示結果.
//
?盡管函數體是相同的,但是作用域是不同的,所以每次調用返回不同的結果:
alert(a[
0
]());??
//
?Displays?0
alert(a[
1
]());??
//
?Displays?1
alert(a[
2
]());??
//
?Displays?2
??????? 這段代碼的結果是正確的,是根據詞法作用域規(guī)則的嚴謹的應用所期待的:函數被執(zhí)行在它被定義的作用域內.然而,這些結果令人吃驚的原因是,你期待的局部作用域在定義它們的函數退出的時候就不存在了.事實上,這是正常現象.當函數被調用的時候,解釋器創(chuàng)建一個調用對象并把它放到作用域鏈的頭部.當函數退出的時候,解釋器從作用域鏈上刪除這個調用對象.在沒有嵌套函數被定義的時候,調用對象是唯一引用作用域鏈的對象.當調用對象從作用域鏈上刪除時,就再也沒有對它的引用了,它將被GC(garbage collected)回收.
??????? 但是,嵌套函數改變了這些.如果嵌套函數被創(chuàng)建,這個函數的定義引用調用對象,因為這個調用對象是函數被定義的作用域鏈的頂部.如果嵌套函數只是被外層函數使用,對嵌套函數的唯一引用在調用對象里.當外層函數返回時,只有嵌套函數引用調用對象,調用對象引用嵌套函數,除此之外,再也沒有其它的什么引用任何一個,因此,這兩個對象就只能被GC使用了.
??????? 如果你保存了一個嵌套函數的引用到全局作用域,情況就有所不同了.你把嵌套函數作為外層函數的返回值,或者把嵌套函數保存為其它對象的屬性.在這種情況下,就有了一個對嵌套函數的外部引用,所以,嵌套函數在它的外部函數的調用對象中保持著它的引用.結果是,為外層函數調用生成的調用對象仍然有效,外層函數的參數和變量的名字和值也保留在這個調用對象里.JavaScript代碼無法直接訪問調用對象,但是,它定義的作為作用域鏈的一部分的屬性仍用于嵌套函數的任何調用.(注意:如果外層函數保存了兩個嵌套函數的全局引用,那么就有兩個嵌套函數共享同一個調用對象,通過調用一個函數對調用對象的改變對另一個嵌套函數是可見的)
??????? JavaScript函數是被執(zhí)行的代碼和執(zhí)行它們的作用域的組合.這個代碼和作用域的組合在計算機科學著作中被稱作:閉包(closure).所有的JavaScript函數都是閉包.然而,這些閉包只在象上面討論的那樣時才有趣:當一個嵌套的函數被輸出到它被定義的作用域之外.只有嵌套函數被如此使用時,才被明確的稱為閉包.
??????? 閉包是有趣并且功能強大的技術.盡管它們不會被普通的使用在日常JavaScript編程中,它仍然值得我們去理解.如果你理解閉包,你理解作用域鏈和函數調用對象,那么,你才能真正的稱自己為高級JavaScript程序員(JSer :) ).
8.8.4.1. 閉包的例子(Closure examples)
??????? 有時,你會想寫一個函數,希望它能跨調用保存一個值.這個值不能保存在局部變量里,因為調用對象不會跨調用存在.全局變量是可以的,但是它會破壞全局命名空間.在8.6.3.章節(jié)中,我展現了一個名為uniqueInteger()的函數,它用一個屬性保存這個恒久的值.你可以用閉包更進一步實現,創(chuàng)建一個恒久的私有的變量.下面是不用閉包寫的一個函數:
//
?每次調用返回一個不同的整數
uniqueID?
=
?
function
()?
{
????
if
?(
!
arguments.callee.id)?arguments.callee.id?
=
?
0
;
????
return
?arguments.callee.id
++
;
}
;
??????? 這種方法的問題在于任何人都能設置這個uniqueID.id為0,而破壞了該函數不能返回同一個值兩次的約定.你可以通過保存這個恒久值到一個只有你自己的函數有權訪問的閉包里的方法來防止別人設置:
uniqueID?
=
?(
function
()?
{??
//
?這個函數的調用對象保存值
????
var
?id?
=
?
0
;???????????
//
?這是私有恒久的那個值
????
//
?外層函數返回一個有權訪問恒久值的嵌套的函數
????
//
?那就是我們保存在變量uniqueID里的嵌套函數.
????
return
?
function
()?
{?
return
?id
++
;?}
;??
//
?返回,自加.
}
)();?
//
?在定義后調用外層函數.
??????? 例子8-6是第二個閉包的例子.它示范的是像第一個一樣的私有恒久變量,但是這個能被多個函數共享.
??????? Example 8-6. Private properties with closures
//
?這個函數為對象o的指定名稱的屬性添加了訪問方法
//
?方法名為:get<name>和set<name>.
//
?如果提供了一個判斷函數,setter方法將在保存前判斷參數是不是有效的
//
?如果檢驗失敗,setter方法拋出一個異常
//
?這個函數的與眾不同之處在于,用getter和setter方法操作的屬性值并不是存儲在對象o里面,
//
?相反的,值被存儲在函數的局部變量里.
//
?getter和setter方法也被定義為函數的局部方法,因此有權訪問這個局部變量.
//
?注意:對于兩個訪問方法,該值是私有的,除了setter方法,無法修改或設置它.
function
?makeProperty(o,?name,?predicate)?
{
????
var
?value;??
//
?This?is?the?property?value
????
//
?getter方法只是簡單的返回值.
????o[
"
get
"
?
+
?name]?
=
?
function
()?
{?
return
?value;?}
;

????
//
?setter保存值,如果校驗失敗則拋出異常
????o[
"
set
"
?
+
?name]?
=
?
function
(v)?
{
????????
if
?(predicate?
&&
?
!
predicate(v))
????????????
throw
?
"
set
"
?
+
?name?
+
?
"
:?invalid?value?
"
?
+
?v;
????????
else
????????????value?
=
?v;
????}
;
}
//
?下面的代碼演示makeProperty()?方法.
var
?o?
=
?
{}
;??
//
?這是一個空對象
//
?添加屬性訪問方法getName()?和?setName()
//
?確保只允許字符串值
makeProperty(o,?
"
Name
"
,?
function
(x)?
{?
return
?
typeof
?x?
==
?
"
string
"
;?}
);

o.setName(
"
Frank
"
);??
//
?設置屬性值
print(o.getName());??
//
?獲得屬性值
o.setName(
0
);????????
//
?試圖設置錯誤類型的值
??????? 我知道的最簡單最有用的使用閉包的例子是Steve Yen創(chuàng)建的斷點程序,它發(fā)布在
http://trimpath.com
,是TrimPath客戶端框架的一部分.斷點是函數內的一個點,代碼執(zhí)行到該點停止,給程序員檢查變量,表達式,調用函數等的值的機會.Steve的斷點技術用閉包捕捉函數的當前作用域(包括局部變量和函數參數),用全局的eval()函數組合這些就可以檢查作用域了.eval()函數計算JavaScript代碼字符串并返回結果.下面是一個以自檢閉包方式工作的嵌套函數.
// 捕捉當前作用域,可以用eval()檢查
var inspector = function($) { return eval($); }
??????? 這個函數用了很少見的標識符$作為參數名,這樣可以減少在計劃檢查的作用域內命名沖突的可能性.
??????? (接下來部分代碼與所述內容無關,譯略)
8.8.4.2. 閉包和IE中的內存泄露(Closures and memory leaks in Internet Explorer)
??????? MS的IE瀏覽器在ActiveX對象和客戶端DOM元素的GC方面表現較弱.客戶端對象按引用計數,當引用數為0的時候釋放對象.這種方法在循環(huán)引用的時候就失效了,例如,當一個核心JavaScript對象引用一個文檔元素,而那個文檔元素又有一個屬性(比如是一個事件句柄)引用該核心JavaScript對象.
??????? 在IE客戶端編程使用閉包的時候,這種循環(huán)引用經常出現.當你使用閉包的時候,記住,封閉(enclosing)函數的調用對象,包括函數所有的參數和局部變量,都將和閉包一樣"長壽".如果任何函數參數或者局部變量引用了一個客戶端對象,就會發(fā)生內存泄露.
??????? 關于這個問題的完整討論超出本書范圍,詳情請參見:?
http://msdn.microsoft.com/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
本文出處:JavaScript - the definitive guide,5th edition
譯: 梅雪香
時間:2006-10-29
posted on 2006-10-29 18:38
梅雪香 閱讀(1281)
評論(0) 編輯 收藏