隨筆 - 16  文章 - 42  trackbacks - 0
          <2006年10月>
          24252627282930
          1234567
          891011121314
          15161718192021
          22232425262728
          2930311234

          失業(yè)中…

          常用鏈接

          留言簿(7)

          隨筆檔案(16)

          技術(shù)Blog

          搜索

          •  

          最新隨筆

          最新評論

          閱讀排行榜

          評論排行榜

          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ù)時,當前作用域鏈被保存起來并成為該函數(shù)內(nèi)部狀態(tài)的一部分.作用域鏈的頂層(最初一層)是由全局對象構(gòu)成的,這和詞法作用域沒什么明顯的關(guān)聯(lián).然而,當你定義一個嵌套函數(shù)時,作用域鏈將包含外層函數(shù)(嵌套函數(shù)的外層函數(shù).原文:the containing function).這就意味著,被嵌套的函數(shù)可以訪問外層函數(shù)的所有參數(shù)和局部變量.

          ??????? 注意:盡管在一個函數(shù)定義的時候,作用域鏈就已經(jīng)固定了,但是作用域鏈中定義的屬性并不是固定的.作用域鏈是"活的"("live"),當函數(shù)被調(diào)用的時候,它有權(quán)訪問任何當前被關(guān)聯(lián)的數(shù)據(jù).

          8.8.2. 調(diào)用對象(The Call Object)
          ??????? 當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中.當f被調(diào)用的時候,作用域鏈由為函數(shù)f調(diào)用生成的調(diào)用對象跟隨在全局對象之后構(gòu)成.g函數(shù)被定義在f函數(shù)里,因此,這個作用域鏈作為g函數(shù)定義的一部分被保存起來.當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ī)則的嚴謹?shù)膽?yīng)用所期待的:函數(shù)被執(zhí)行在它被定義的作用域內(nèi).然而,這些結(jié)果令人吃驚的原因是,你期待的局部作用域在定義它們的函數(shù)退出的時候就不存在了.事實上,這是正常現(xiàn)象.當函數(shù)被調(diào)用的時候,解釋器創(chuàng)建一個調(diào)用對象并把它放到作用域鏈的頭部.當函數(shù)退出的時候,解釋器從作用域鏈上刪除這個調(diào)用對象.在沒有嵌套函數(shù)被定義的時候,調(diào)用對象是唯一引用作用域鏈的對象.當調(diào)用對象從作用域鏈上刪除時,就再也沒有對它的引用了,它將被GC(garbage collected)回收.

          ??????? 但是,嵌套函數(shù)改變了這些.如果嵌套函數(shù)被創(chuàng)建,這個函數(shù)的定義引用調(diào)用對象,因為這個調(diào)用對象是函數(shù)被定義的作用域鏈的頂部.如果嵌套函數(shù)只是被外層函數(shù)使用,對嵌套函數(shù)的唯一引用在調(diào)用對象里.當外層函數(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í)行它們的作用域的組合.這個代碼和作用域的組合在計算機科學著作中被稱作:閉包(closure).所有的JavaScript函數(shù)都是閉包.然而,這些閉包只在象上面討論的那樣時才有趣:當一個嵌套的函數(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ù)的當前作用域(包括局部變量和函數(shù)參數(shù)),用全局的eval()函數(shù)組合這些就可以檢查作用域了.eval()函數(shù)計算JavaScript代碼字符串并返回結(jié)果.下面是一個以自檢閉包方式工作的嵌套函數(shù).


          // 捕捉當前作用域,可以用eval()檢查
          var inspector = function($) { return eval($); }

          ??????? 這個函數(shù)用了很少見的標識符$作為參數(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ù),當引用數(shù)為0的時候釋放對象.這種方法在循環(huán)引用的時候就失效了,例如,當一個核心JavaScript對象引用一個文檔元素,而那個文檔元素又有一個屬性(比如是一個事件句柄)引用該核心JavaScript對象.

          ??????? 在IE客戶端編程使用閉包的時候,這種循環(huán)引用經(jīng)常出現(xiàn).當你使用閉包的時候,記住,封閉(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 梅雪香 閱讀(1289) 評論(0)  編輯  收藏

          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導航:
           
          主站蜘蛛池模板: 胶州市| 正蓝旗| 福建省| 浪卡子县| 清水河县| 阿尔山市| 寿宁县| 崇信县| 奉贤区| 江川县| 凤城市| 锦屏县| 新源县| 广平县| 汝城县| 梓潼县| 合山市| 南充市| 探索| 武威市| 白河县| 嘉祥县| 四平市| 通州区| 古浪县| 平阴县| 泊头市| 凌云县| 井冈山市| 垣曲县| 南丹县| 顺平县| 高安市| 毕节市| 桦川县| 常德市| 丰原市| 新营市| 武威市| 皮山县| 玉屏|