做用域鏈–JS基礎核心之一

JS中的做用域,你們都知道的,分爲全局做用域和局部做用域,沒有塊級做用域,聽起來其實很簡單的,但是做用域是否可以有深刻的瞭解,對於JS代碼邏輯的編寫成功率,BUG的解決能力,以及是否能寫出更優秀的代碼,都有很重要的影響的,若是想要寫出更優雅更高效的邏輯代碼,那麼就要深刻的瞭解一下做用域的問題了,確切的說,是要更深刻的瞭解一下,怎麼更有效更巧妙的利用做用域。前端

全局和局部做用域

這個我以爲吧,只要學習過編程語言的,就會對這些有簡單的瞭解的。好比在JS語言中,屬於window對象的屬性和方法,是能夠被咱們自定義的函數或者方法的局部做用域訪問的,而咱們自定義的函數和對象內部的屬性和方法,卻只能在內部使用。這裏,window對象就是在全局做用域中,而咱們自定義的函數或者對象內部,就是局部做用域。編程

  • var num = 1;
  • function changeNum(){
  •     var str = "zhang";
  •     num = 2;
  • }
  • console.log(num); //1
  • console.log(typeof str);//undefined
  • changeNum();
  • console.log(num); //2
  • console.log(typeof str);//undefined

上述代碼中,之因此要使用typeof str,是由於對於沒有定義的變量,瀏覽器會拋出錯誤,而且阻塞瀏覽器繼續執行後續代碼的。瀏覽器

注:若是肯定要定義爲局部變量,那麼千萬不要忘記使用 var 操做符哦。

局部做用域的位置通常是在函數或者對象內部,爲了敘述方便,接下來就只以函數的局部做用域來進行分析說明。閉包

在函數中使用var操做符定義一個變量,那麼當這個函數執行完畢以後,這個變量也會被銷燬(也有的狀況下不會,好比閉包,後面會說明),而全局變量會一直存在。因此在咱們寫代碼時,儘可能少的使用全局變量,濫用全局變量,簡直就是一個會使人噁心的習慣,由於它會帶來不少沒必要要的麻煩。app

  • 1:變量過多,命名麻煩
  • 2:局部變量,忘記使用var定義,修改了全局變量,這樣的錯誤對於代碼的維護簡直是噩夢
  • 3:全局變量會在頁面卸載前一直存在,損耗沒必要要的內存。

暫時就想到這些,反正就是儘可能少用就對了。。。。編程語言

做用域鏈

引自Javascript高級程序設計(第三版)(P73):當代碼在一個環境中執行時,會建立變量對象的的一個做用域鏈(scope chain)。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是一個函數,則將其活動對象做爲變量對象。函數

每個函數都有本身的執行環境,當執行流進一個函數時,函數環境就會被推入一個環境棧中,而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境,這個棧也就是做用域鏈。學習

上面寫了那麼多,在我看起來能夠用下面的簡單代碼來表達:測試

  • var a = 1;
  • //全局做用域,只能訪問全局變量,也就是a變量
  • function A(){
  •     var b = 2;
  •     //A函數的局部做用域,能夠訪問到a,b變量,可是訪問不到c變量
  •     function B(){
  •         //B函數局部做用域,能夠訪問到a,b,c變量
  •         var c = 3;
  •     }
  • }

很明顯的,貌似做用域方面,也沒有什麼好說的。但是,有時候,咱們卻不得不去訪問一些局部做用域內部的東西,好比兩個模塊函數,使用了相同的數據,這裏咱們也只能把這些相同的數據放入全局變量,使得兩個函數模塊,均可以調用這些數據。動畫

可是想一想,若是這樣的需求不少,那麼不久須要不少不少的全局變量,而濫用全局變量的很差之處,前面也說了,因此這並非一種好的寫法。

減小全局變量

減小全局變量的方法,其實也不少,好比把一些相同類型的全局變量存入一個對象,那麼就能夠把這些類型的N多個全局變量,變成一個全局的對象,以後按照對象訪問便可。

固然,我以爲吧,最簡單,又好用的,仍是在一個函數內部,繼續定義函數,就像以前在函數A內部,定義了函數B,這樣咱們只須要一個函數A的執行,就能夠完成一整個邏輯。內部的調用,都只能算是局部變量的調用,在全局只添加了一個函數A

好比:

  • function A(){
  •     var arr = [];
  •     function a(){};
  •     function b(){};
  •     
  •     return;
  • }

這樣,咱們原本須要三個全局變量的問題,就變成了只須要一個。固然,如何減小全局變量的方法是有不少種的,這裏不作討論。

這裏,咱們就討論一種咱們最多見的方法,也算是很經常使用的一種代碼書寫方法吧,它叫:閉包。

減小全局變量方法–閉包

說到閉包,咱們首先來看一個最最簡單的例子,也是最最基礎的例子:爲多個相同的元素,綁定事件,在點擊每個元素時,提示被點擊元素的排列位置。

  • <div id = "test">
  •     <p>欄目1</p>
  •     <p>欄目2</p>
  •     <p>欄目3</p>
  •     <p>欄目4</p>
  • </div>

這樣的結構

  • function bindClick(){
  •     var allP = document.getElementById("test").getElementsByTagName("p"),
  •         i=0,
  •         len = allP.length;
  •         
  •     for( ;i<len;i++){
  •         allP[i].onclick = function(){
  •             alert("you click the "+i+" P tag!");
  •             //you click the 4 P tag!
  •         }
  •     }
  • }
  • bindClick();
  • //運行函數,綁定點擊事件

這樣的JS處理,看起來沒有問題,但是在測試的時候,無論咱們點擊哪個p標籤,咱們獲取到的結果都是相同的,tell me why?說白了,這就是做用域到致使的一個問題。

下面來分析一下緣由。首先呢,咱們先把上述的JS代碼給分解一下,讓咱們看起來更容易理解。

  • function bindClick(){
  •     var allP = document.getElementById("test").getElementsByTagName("p"),
  •         i=0,
  •         len = allP.length;
  •     
  •     for( ;i<len;i++){
  •         allP[i].onclick = AlertP;
  •     }
  •     function AlertP(){
  •         alert("you click the "+i+" P tag!");
  •     }
  • }
  • bindClick();
  • //運行函數,綁定點擊事件

這裏應該沒有什麼問題吧,前面使用一個匿名函數做爲click事件的回調函數,這裏使用的一個非匿名函數,做爲回調,徹底相同的效果。也能夠作下測試哦。

理解上面的說法了,那麼就能夠很簡單的理解,爲何咱們以前的代碼,會獲得一個相同的結果了。首先看一下for循環中,這裏咱們只是對每個匹配的元素添加了一個click的回調函數,而且回調函數都是AlertP函數。這裏當爲每個元素添加成功click以後,i的值,就變成了匹配元素的個數,也就是i=len,而當咱們觸發這個事件時,也就是當咱們點擊相應的元素時,咱們期待的是,提示出咱們點擊的元素是排列在第幾個,這個時候,click事件觸發,執行回調函數AlertP,可是當執行到這裏的時候,發現alert方法中,有一個變量是未知的,而且在AlertP的局部做用域中,也沒有查找到相應的變量,那麼按照做用域鏈的查找方式,就會向父級做用域去查找,這裏的父級做用域中,確實是有變量i的,而i的值,倒是通過for循環以後的值,i=len。因此也就出現了咱們最初看到的效果。

瞭解了這裏的緣由,那麼解決方法也就很簡單了,控制這個做用域的問題唄,說白了,也就一個方法,那就是在回調函數中,用一個局部變量,來記錄這個i的值,這樣當再局部做用域中使用到i變量時,就會使用優先使用局部變量中的i變量的值。不會再去查找全局變量了。

因此呢,理解了這兩段文字,那麼若是我把代碼寫成下面的樣式:

  • function bindClick(){
  •     var allP = document.getElementById("test").getElementsByTagName("p"),
  •         i=0,
  •         len = allP.length;
  •     
  •     for( ;i<len;i++){
  •         allP[i].onclick = AlertP;
  •     }
  • }
  • function AlertP(){
  •     alert("you click the "+i+" P tag!");
  • }
  • bindClick();
  • //運行函數,綁定點擊事件

分析一下,若是這段代碼這樣寫,那麼結果會是如何呢?

說到了這裏,大概也能理解一下閉包的概念了,按照以前咱們說的做用域鏈的說法,當一個函數運行時,該函數就會被推入做用域鏈的前端,當函數執行結束,這個函數就會被推出做用域鏈,而且銷燬函數內部的局部變化和方法。

可是這裏呢,當bindClick運行結束後,依然能夠經過click事件訪問到bindClick函數內部的i變量,說明bindClick函數內部的i變量,在bindClick結束後,並無被銷燬,這也就是閉包了。

2014.10.19-PS:發現上面的這段代碼,是有問題的,這樣的寫法,在運行時,i的值會一直是undefined,由於這個時候,i是在AlertP內部和全局做用域中查找,而這兩個做用域中,並無i的定義,正確的寫法,在文章的後面有說明,因此如今想不到當時爲何會這麼寫了。。汗一個~~
PS:閉包,說白了也就是在函數執行結束,做用域鏈將函數彈出以後,函數內部的一些變量或者方法,還能夠經過其餘的方法引用。

OK,回到正題,這裏既然知道了須要一個局部變量的i值,能夠解決這個問題,那麼方法也就很簡單了,按咱們以前說的,變量按照可訪問性的話,只分爲全局變量和局部變量,那麼這裏的就很簡單了,使用一個函數,構造一個局部變量便可。

方法1:使得綁定click事件的目標對象和變量i都變成局部變量。這裏能夠直接把這二者做爲形參,傳遞給另外的一個函數便可。

  • function bindClick(){
  •     var allP = document.getElementById("test").getElementsByTagName("p"),
  •         i=0,
  •         len = allP.length;
  •     
  •     for( ;i<len;i++){
  •         AlertP(allP[i],i);
  •     }
  •     
  •     function AlertP(obj,i){
  •         obj.onclick = function(){
  •             alert("you click the "+i+" P tag!");
  •         }
  •     }
  • }
  • bindClick();

這裏,objiAlertP函數內部,就是局部變量了。click事件的回調函數,雖然依舊沒有變量i的值,可是其父做用域AlertP的內部,倒是有的,因此能正常的顯示了,這裏AlertP我放在了bindClick的內部,只是由於這樣能夠減小必要的全局函數,放到全局也不影響的。

這裏是添加了一個函數進行綁定,若是我不想添加函數呢,固然也能夠實現了,這裏就要說到自執行函數了。說到自執行函數,不知道你們有什麼理解,曾經有段事件,我實在是理解不到那種寫法,爲什麼叫作自執行函數,這裏也順便帶一筆了。

有沒有人,在剛開始接觸到JS時,會這樣綁定事件:obj.onclick = callback();

而後出錯了卻一直找不到錯誤在哪裏,後來才以後,當一個函數名添加了括號以後,就是函數執行了,那麼也就明白了,上面的寫法,其實就是把callback函數執行後的返回結果做爲了objclick事件的回調函數了。

而函數名的話,也就是一個function函數的引用吧,根據函數名查找到對應的function處理模塊,因此這裏很容易的也就想到了,自執行函數也就是直接在一個匿名函數的後面添加一對小括號,那麼這個匿名函數就會本身執行了。因此也就是自執行函數了。

好比咱們在頁面加載以後,想要當即提示用戶,頁面加載完畢,咱們習慣於這麼寫:

  • function loadSuccess(){
  •     alert("page onload success!");
  • }
  • loadSuccess();

這是咱們經常使用的方法,這裏首先定義個函數,並把函數名命名爲loadSuccess,以後調用這個函數。很經常使用很簡單。

這裏咱們一般也可使用自執行函數來完成這個提示,你就能夠這樣寫:

  • (function(){
  •     alert("page onload success!");
  • })();

完成相同的功能,這裏必須把這個匿名函數放在小括號內部,否則瀏覽器會報錯的。

緣由呢,也是JS中的常識之一,那就是function A(){}這樣的定義函數的方法,會在瀏覽器進行預編譯的時候進行解析,而var A = function(){}這樣的定義函數的方法,則是當JS解析到該行代碼時,纔會被解析。

這裏呢,若是在上面的自執行函數中,不添加第一個小括號,瀏覽器就會在預編譯時,對該部分進行解析,可是這個時候,由於沒有對這部分function進行命名,瀏覽器在預編譯時就會報錯,而致使沒法進行下去了。

使用下面這段函數,就能夠證實,是在預編譯的時候,報錯的而致使沒法執行的

  • alert("123");
  • function(){
  •     alert("page onload success!");
  • }();

固然啦,加括號本就不是必須的,好比咱們使用表達式定義函數時,var A = function(){}這種寫法,就不是在預編譯的時候進行的,因此,若是咱們的自執行函數會把返回值定義到另一個變量,是能夠省略掉小括號的。

好比:

  • alert("123");
  • var a = function(){
  •     alert("page onload success!");
  • }();

這樣寫也會連續有兩個alert執行,完成咱們以前說的功能,也不會報錯,只是這時,自執行函數是沒有返回值的,因此最後的a變量,是undefined。不過呢,爲了統一塊兒見,也爲了看着方便,因此仍是對各類寫法的自執行函數的寫法,都添加上小括號吧。

至於爲何,添加了小括號()(),這樣寫,就能夠,那就是由於,這樣的寫法就變成一個表達式了。。。。

能夠這麼證實一下:

  • (function A(){
  •     alert("page onload success!");
  • });
  • A();

只是這樣的寫法,和表達式定義函數就相似了,並且還會有一個問題就是,A函數,只有在這個括號內部使用。在外部使用,須要先把這個表達式進行賦值才行,若是賦值,那不就是成了使用賦值表達式定義函數了。

說的遠了點,回來繼續:到這裏也大概瞭解了自執行函數的執行方法了吧。那使用自執行函數的方法,進行事件的綁定,大概也能猜到它的原理了吧。obj.onclick = callback();。若是我把callback函數的返回值,定義成一個函數,那當click事件觸發時,不就是觸發了這個返回的函數了。

因此呢,咱們能夠這樣寫:

  • function bindClick(){
  •     var allP = document.getElementById("test").getElementsByTagName("p"),
  •         i=0,
  •         len = allP.length;
  •     
  •     for( ;i<len;i++){
  •         allP[i].onclick = AlertP(i);
  •     }
  • }
  • function AlertP(i){
  •     return function(){
  •         alert("you click the "+i+" P tag!");
  •     }
  • }
  • bindClick();

沒有什麼問題吧?應該很容易理解到吧。

但是這樣的寫法呢,添加了一個函數變量,若是不添加呢。。。OK的,把後面的函數直接替換過去就好了。。。。

  • function bindClick(){
  •     var allP = document.getElementById("test").getElementsByTagName("p"),
  •         i=0,
  •         len = allP.length;
  •     
  •     for( ;i<len;i++){
  •         allP[i].onclick = function (i){
  •             return function(){
  •                 alert("you click the "+i+" P tag!");
  •             }
  •         }(i);
  •     }
  • }
  • bindClick();

這樣看起來,對比以前的寫法,應該就能很明顯的瞭解到,爲何這麼寫,能獲得咱們想要的結果了吧。

OK,這也是閉包的最簡單的應用了,其餘的閉包寫法也有,只是就原理方面來講,和上面這種是相同的原理,因此這裏就不一一列舉了,用到閉包的地方其實不少(好比惰性載入函數,單例模式中的對象定義等),若是您能理解到這最簡單閉包的原理,那麼其餘用到閉包的地方,見到了,也就能理解了。或者說,想要使用的時候,也就能想到應該怎麼用了吧。

以前的文章中,也有一篇文章中的代碼,主要就是使用的閉包的思想,能夠參考:jQuery源碼學習(二)–proxy

備註

計時器在一些動態頁面,作一些動畫效果時,是不可或缺的一個元素,它和alert方法相同,都是屬於window對象的方法。使用計時器時,是有少量差異的,這裏就以setTimeout爲例簡單說明:

看例子:代碼中中的兩個setTimeout執行後的結果分別是什麼?

  • var a = 1;
  • function B(){
  •     var a = 2;
  •     setTimeout("C()",1000);
  •     setTimeout(C,2000);
  •     function C(){
  •         alert("a="+a);
  •     }
  • }
  • function C(){
  •     alert("a="+a);
  • }
  • B();

測試一下也就知道了,分別爲12,由於setTimeout是把後面執行的方法,第一種寫法,只會查找全局變量中,是否有A函數,而第二種寫法,會優先查找當前做用域中是否有A函數,若是局部沒有的話,則順序查找到全局做用域中。

有一種狀況,是說,計時器內部調用的函數的this指向,是指向window的,這裏能夠說有錯,也能夠說沒錯,看一個例子:假設給id=test的一個元素綁定一個click事件。查看其中的this的值。

  • document.getElementById("test").onclick = function(){
  •     alert(this); //指向觸發該事件的元素對象
  •     setTimeout("A()",1000); //這裏調用指向window
  • }
  • function A(){
  •     alert(this);
  • }

這裏就不考慮在IE8-的瀏覽器了。

按照最初寫的兩個計時器的例子,在寫出以下的代碼:

  • document.getElementById("test").onclick = function() {
  •     alert(this); //指向觸發該事件的元素對象
  •     setTimeout(A,1000); ////這裏依然指向window
  •     function A(){
  •         alert(this);
  •     }
  • };
  • function A(){
  •     alert(this);
  • }

爲何?不是按理說,這裏應該是調用的內部的A方法嗎?爲何this倒是指向的window

有一個不肯定的想法是:當調用了計時器時,會把當前做用域中的方法,內部的this指向window對象了。並且僅僅是修改了方法內部的this指向,若是有私有變量的取值,依然按照原函數所在的位置,根據做用域,進行取值。

能夠這麼證實一下:

  • var a = 1;
  • document.getElementById("test").onclick = function() {
  •     alert(this);
  •     var a = 123;
  •     setTimeout(A,1000);
  •     function A(){
  •         alert("a="+a);
  •         alert(this);
  •     }
  • }
  • function A(){
  •     alert("a="+a);
  •     alert(this);
  • }

this的指向是和上面一個實例相同的,而alert中的a變量的取值,倒是優先獲取局部做用域中的值。

固然啦,這裏若是把計時器中的調用方法,更換一下,那結果就不相同了哦。

  • var a = 1;
  • document.getElementById("test").onclick = function() {
  •     alert(this);
  •     var a = 123;
  •     setTimeout("A()",1000);
  •     function A(){
  •         alert("a="+a);
  •         alert(this);
  •     }
  • }
  • function A(){
  •     alert("a="+a);
  •     alert(this);
  • }

這裏,有興趣的能夠試試吧,說到這裏,也發現,雖然使用計時器會強制把調用函數的內部的this指向改變成指向window的,可是對於做用域鏈的影響卻只有寫法不一樣帶來的影響。即:setTimeout("A()",1000);setTimeout(A,1000);的不一樣。固然對於第二種寫法,咱們可使用callapply強行改變A內部this的指向,不過這些跟本文的內容,貌似沒有什麼關係,就很少說了。

其實,按照我原本的想法,這裏該寫一下計時器(setTimeout,setInterval)和call,apply這幾個和做用域鏈的關係,可是寫到這裏,又感受他們的並無什麼關係,因此關於做用域鏈,就到這裏。

OK了,若是您有什麼新的想法,或者認識,或者發現文中的錯誤,請指教,很是感謝!

本文地址:http://www.zhangyunling.com/?p=134

相關文章
相關標籤/搜索