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

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

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

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

//
?現(xiàn)在調(diào)用這些函數(shù)并顯示結(jié)果.
//
?盡管函數(shù)體是相同的,但是作用域是不同的,所以每次調(diào)用返回不同的結(jié)果:
alert(a[
0
]());??
//
?Displays?0
alert(a[
1
]());??
//
?Displays?1
alert(a[
2
]());??
//
?Displays?2
??????? 這段代碼的結(jié)果是正確的,是根據(jù)詞法作用域規(guī)則的嚴(yán)謹(jǐn)?shù)膽?yīng)用所期待的:函數(shù)被執(zhí)行在它被定義的作用域內(nèi).然而,這些結(jié)果令人吃驚的原因是,你期待的局部作用域在定義它們的函數(shù)退出的時候就不存在了.事實上,這是正常現(xiàn)象.當(dāng)函數(shù)被調(diào)用的時候,解釋器創(chuàng)建一個調(diào)用對象并把它放到作用域鏈的頭部.當(dāng)函數(shù)退出的時候,解釋器從作用域鏈上刪除這個調(diào)用對象.在沒有嵌套函數(shù)被定義的時候,調(diào)用對象是唯一引用作用域鏈的對象.當(dāng)調(diào)用對象從作用域鏈上刪除時,就再也沒有對它的引用了,它將被GC(garbage collected)回收.
??????? 但是,嵌套函數(shù)改變了這些.如果嵌套函數(shù)被創(chuàng)建,這個函數(shù)的定義引用調(diào)用對象,因為這個調(diào)用對象是函數(shù)被定義的作用域鏈的頂部.如果嵌套函數(shù)只是被外層函數(shù)使用,對嵌套函數(shù)的唯一引用在調(diào)用對象里.當(dāng)外層函數(shù)返回時,只有嵌套函數(shù)引用調(diào)用對象,調(diào)用對象引用嵌套函數(shù),除此之外,再也沒有其它的什么引用任何一個,因此,這兩個對象就只能被GC使用了.
??????? 如果你保存了一個嵌套函數(shù)的引用到全局作用域,情況就有所不同了.你把嵌套函數(shù)作為外層函數(shù)的返回值,或者把嵌套函數(shù)保存為其它對象的屬性.在這種情況下,就有了一個對嵌套函數(shù)的外部引用,所以,嵌套函數(shù)在它的外部函數(shù)的調(diào)用對象中保持著它的引用.結(jié)果是,為外層函數(shù)調(diào)用生成的調(diào)用對象仍然有效,外層函數(shù)的參數(shù)和變量的名字和值也保留在這個調(diào)用對象里.JavaScript代碼無法直接訪問調(diào)用對象,但是,它定義的作為作用域鏈的一部分的屬性仍用于嵌套函數(shù)的任何調(diào)用.(注意:如果外層函數(shù)保存了兩個嵌套函數(shù)的全局引用,那么就有兩個嵌套函數(shù)共享同一個調(diào)用對象,通過調(diào)用一個函數(shù)對調(diào)用對象的改變對另一個嵌套函數(shù)是可見的)
??????? JavaScript函數(shù)是被執(zhí)行的代碼和執(zhí)行它們的作用域的組合.這個代碼和作用域的組合在計算機科學(xué)著作中被稱作:閉包(closure).所有的JavaScript函數(shù)都是閉包.然而,這些閉包只在象上面討論的那樣時才有趣:當(dāng)一個嵌套的函數(shù)被輸出到它被定義的作用域之外.只有嵌套函數(shù)被如此使用時,才被明確的稱為閉包.
??????? 閉包是有趣并且功能強大的技術(shù).盡管它們不會被普通的使用在日常JavaScript編程中,它仍然值得我們?nèi)ダ斫?如果你理解閉包,你理解作用域鏈和函數(shù)調(diào)用對象,那么,你才能真正的稱自己為高級JavaScript程序員(JSer :) ).
8.8.4.1. 閉包的例子(Closure examples)
??????? 有時,你會想寫一個函數(shù),希望它能跨調(diào)用保存一個值.這個值不能保存在局部變量里,因為調(diào)用對象不會跨調(diào)用存在.全局變量是可以的,但是它會破壞全局命名空間.在8.6.3.章節(jié)中,我展現(xiàn)了一個名為uniqueInteger()的函數(shù),它用一個屬性保存這個恒久的值.你可以用閉包更進一步實現(xiàn),創(chuàng)建一個恒久的私有的變量.下面是不用閉包寫的一個函數(shù):
//
?每次調(diào)用返回一個不同的整數(shù)
uniqueID?
=
?
function
()?
{
????
if
?(
!
arguments.callee.id)?arguments.callee.id?
=
?
0
;
????
return
?arguments.callee.id
++
;
}
;
??????? 這種方法的問題在于任何人都能設(shè)置這個uniqueID.id為0,而破壞了該函數(shù)不能返回同一個值兩次的約定.你可以通過保存這個恒久值到一個只有你自己的函數(shù)有權(quán)訪問的閉包里的方法來防止別人設(shè)置:
uniqueID?
=
?(
function
()?
{??
//
?這個函數(shù)的調(diào)用對象保存值
????
var
?id?
=
?
0
;???????????
//
?這是私有恒久的那個值
????
//
?外層函數(shù)返回一個有權(quán)訪問恒久值的嵌套的函數(shù)
????
//
?那就是我們保存在變量uniqueID里的嵌套函數(shù).
????
return
?
function
()?
{?
return
?id
++
;?}
;??
//
?返回,自加.
}
)();?
//
?在定義后調(diào)用外層函數(shù).
??????? 例子8-6是第二個閉包的例子.它示范的是像第一個一樣的私有恒久變量,但是這個能被多個函數(shù)共享.
??????? Example 8-6. Private properties with closures
//
?這個函數(shù)為對象o的指定名稱的屬性添加了訪問方法
//
?方法名為:get<name>和set<name>.
//
?如果提供了一個判斷函數(shù),setter方法將在保存前判斷參數(shù)是不是有效的
//
?如果檢驗失敗,setter方法拋出一個異常
//
?這個函數(shù)的與眾不同之處在于,用getter和setter方法操作的屬性值并不是存儲在對象o里面,
//
?相反的,值被存儲在函數(shù)的局部變量里.
//
?getter和setter方法也被定義為函數(shù)的局部方法,因此有權(quán)訪問這個局部變量.
//
?注意:對于兩個訪問方法,該值是私有的,除了setter方法,無法修改或設(shè)置它.
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
"
);??
//
?設(shè)置屬性值
print(o.getName());??
//
?獲得屬性值
o.setName(
0
);????????
//
?試圖設(shè)置錯誤類型的值
??????? 我知道的最簡單最有用的使用閉包的例子是Steve Yen創(chuàng)建的斷點程序,它發(fā)布在
http://trimpath.com
,是TrimPath客戶端框架的一部分.斷點是函數(shù)內(nèi)的一個點,代碼執(zhí)行到該點停止,給程序員檢查變量,表達式,調(diào)用函數(shù)等的值的機會.Steve的斷點技術(shù)用閉包捕捉函數(shù)的當(dāng)前作用域(包括局部變量和函數(shù)參數(shù)),用全局的eval()函數(shù)組合這些就可以檢查作用域了.eval()函數(shù)計算JavaScript代碼字符串并返回結(jié)果.下面是一個以自檢閉包方式工作的嵌套函數(shù).
// 捕捉當(dāng)前作用域,可以用eval()檢查
var inspector = function($) { return eval($); }
??????? 這個函數(shù)用了很少見的標(biāo)識符$作為參數(shù)名,這樣可以減少在計劃檢查的作用域內(nèi)命名沖突的可能性.
??????? (接下來部分代碼與所述內(nèi)容無關(guān),譯略)
8.8.4.2. 閉包和IE中的內(nèi)存泄露(Closures and memory leaks in Internet Explorer)
??????? MS的IE瀏覽器在ActiveX對象和客戶端DOM元素的GC方面表現(xiàn)較弱.客戶端對象按引用計數(shù),當(dāng)引用數(shù)為0的時候釋放對象.這種方法在循環(huán)引用的時候就失效了,例如,當(dāng)一個核心JavaScript對象引用一個文檔元素,而那個文檔元素又有一個屬性(比如是一個事件句柄)引用該核心JavaScript對象.
??????? 在IE客戶端編程使用閉包的時候,這種循環(huán)引用經(jīng)常出現(xiàn).當(dāng)你使用閉包的時候,記住,封閉(enclosing)函數(shù)的調(diào)用對象,包括函數(shù)所有的參數(shù)和局部變量,都將和閉包一樣"長壽".如果任何函數(shù)參數(shù)或者局部變量引用了一個客戶端對象,就會發(fā)生內(nèi)存泄露.
??????? 關(guān)于這個問題的完整討論超出本書范圍,詳情請參見:?
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) 編輯 收藏