JS
中的做用域,你們都知道的,分爲全局做用域和局部做用域,沒有塊級做用域,聽起來其實很簡單的,但是做用域是否可以有深刻的瞭解,對於JS
代碼邏輯的編寫成功率,BUG
的解決能力,以及是否能寫出更優秀的代碼,都有很重要的影響的,若是想要寫出更優雅更高效的邏輯代碼,那麼就要深刻的瞭解一下做用域的問題了,確切的說,是要更深刻的瞭解一下,怎麼更有效更巧妙的利用做用域。前端
這個我以爲吧,只要學習過編程語言的,就會對這些有簡單的瞭解的。好比在JS
語言中,屬於window
對象的屬性和方法,是能夠被咱們自定義的函數或者方法的局部做用域訪問的,而咱們自定義的函數和對象內部的屬性和方法,卻只能在內部使用。這裏,window
對象就是在全局做用域中,而咱們自定義的函數或者對象內部,就是局部做用域。編程
上述代碼中,之因此要使用typeof str
,是由於對於沒有定義的變量,瀏覽器會拋出錯誤,而且阻塞瀏覽器繼續執行後續代碼的。瀏覽器
局部做用域的位置通常是在函數或者對象內部,爲了敘述方便,接下來就只以函數的局部做用域來進行分析說明。閉包
在函數中使用var
操做符定義一個變量,那麼當這個函數執行完畢以後,這個變量也會被銷燬(也有的狀況下不會,好比閉包,後面會說明),而全局變量會一直存在。因此在咱們寫代碼時,儘可能少的使用全局變量,濫用全局變量,簡直就是一個會使人噁心的習慣,由於它會帶來不少沒必要要的麻煩。app
暫時就想到這些,反正就是儘可能少用就對了。。。。編程語言
引自Javascript
高級程序設計(第三版)(P73
):當代碼在一個環境中執行時,會建立變量對象的的一個做用域鏈(scope chain)。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是一個函數,則將其活動對象做爲變量對象。函數
每個函數都有本身的執行環境,當執行流進一個函數時,函數環境就會被推入一個環境棧中,而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境,這個棧也就是做用域鏈。學習
上面寫了那麼多,在我看起來能夠用下面的簡單代碼來表達:測試
很明顯的,貌似做用域方面,也沒有什麼好說的。但是,有時候,咱們卻不得不去訪問一些局部做用域內部的東西,好比兩個模塊函數,使用了相同的數據,這裏咱們也只能把這些相同的數據放入全局變量,使得兩個函數模塊,均可以調用這些數據。動畫
可是想一想,若是這樣的需求不少,那麼不久須要不少不少的全局變量,而濫用全局變量的很差之處,前面也說了,因此這並非一種好的寫法。
減小全局變量的方法,其實也不少,好比把一些相同類型的全局變量存入一個對象,那麼就能夠把這些類型的N多個全局變量,變成一個全局的對象,以後按照對象訪問便可。
固然,我以爲吧,最簡單,又好用的,仍是在一個函數內部,繼續定義函數,就像以前在函數A
內部,定義了函數B
,這樣咱們只須要一個函數A
的執行,就能夠完成一整個邏輯。內部的調用,都只能算是局部變量的調用,在全局只添加了一個函數A
。
好比:
這樣,咱們原本須要三個全局變量的問題,就變成了只須要一個。固然,如何減小全局變量的方法是有不少種的,這裏不作討論。
這裏,咱們就討論一種咱們最多見的方法,也算是很經常使用的一種代碼書寫方法吧,它叫:閉包。
說到閉包,咱們首先來看一個最最簡單的例子,也是最最基礎的例子:爲多個相同的元素,綁定事件,在點擊每個元素時,提示被點擊元素的排列位置。
這樣的結構
這樣的JS
處理,看起來沒有問題,但是在測試的時候,無論咱們點擊哪個p
標籤,咱們獲取到的結果都是相同的,tell me why?說白了,這就是做用域到致使的一個問題。
下面來分析一下緣由。首先呢,咱們先把上述的JS
代碼給分解一下,讓咱們看起來更容易理解。
這裏應該沒有什麼問題吧,前面使用一個匿名函數做爲click
事件的回調函數,這裏使用的一個非匿名函數,做爲回調,徹底相同的效果。也能夠作下測試哦。
理解上面的說法了,那麼就能夠很簡單的理解,爲何咱們以前的代碼,會獲得一個相同的結果了。首先看一下for
循環中,這裏咱們只是對每個匹配的元素添加了一個click
的回調函數,而且回調函數都是AlertP
函數。這裏當爲每個元素添加成功click
以後,i
的值,就變成了匹配元素的個數,也就是i=len
,而當咱們觸發這個事件時,也就是當咱們點擊相應的元素時,咱們期待的是,提示出咱們點擊的元素是排列在第幾個,這個時候,click
事件觸發,執行回調函數AlertP
,可是當執行到這裏的時候,發現alert
方法中,有一個變量是未知的,而且在AlertP
的局部做用域中,也沒有查找到相應的變量,那麼按照做用域鏈的查找方式,就會向父級做用域去查找,這裏的父級做用域中,確實是有變量i
的,而i的值,倒是通過for
循環以後的值,i=len
。因此也就出現了咱們最初看到的效果。
瞭解了這裏的緣由,那麼解決方法也就很簡單了,控制這個做用域的問題唄,說白了,也就一個方法,那就是在回調函數中,用一個局部變量,來記錄這個i
的值,這樣當再局部做用域中使用到i
變量時,就會使用優先使用局部變量中的i
變量的值。不會再去查找全局變量了。
因此呢,理解了這兩段文字,那麼若是我把代碼寫成下面的樣式:
分析一下,若是這段代碼這樣寫,那麼結果會是如何呢?
說到了這裏,大概也能理解一下閉包的概念了,按照以前咱們說的做用域鏈的說法,當一個函數運行時,該函數就會被推入做用域鏈的前端,當函數執行結束,這個函數就會被推出做用域鏈,而且銷燬函數內部的局部變化和方法。
可是這裏呢,當bindClick
運行結束後,依然能夠經過click
事件訪問到bindClick
函數內部的i變量,說明bindClick
函數內部的i
變量,在bindClick
結束後,並無被銷燬,這也就是閉包了。
OK,回到正題,這裏既然知道了須要一個局部變量的i
值,能夠解決這個問題,那麼方法也就很簡單了,按咱們以前說的,變量按照可訪問性的話,只分爲全局變量和局部變量,那麼這裏的就很簡單了,使用一個函數,構造一個局部變量便可。
方法1:使得綁定click
事件的目標對象和變量i都變成局部變量。這裏能夠直接把這二者做爲形參,傳遞給另外的一個函數便可。
這裏,obj
和i
在AlertP
函數內部,就是局部變量了。click
事件的回調函數,雖然依舊沒有變量i
的值,可是其父做用域AlertP
的內部,倒是有的,因此能正常的顯示了,這裏AlertP
我放在了bindClick
的內部,只是由於這樣能夠減小必要的全局函數,放到全局也不影響的。
這裏是添加了一個函數進行綁定,若是我不想添加函數呢,固然也能夠實現了,這裏就要說到自執行函數了。說到自執行函數,不知道你們有什麼理解,曾經有段事件,我實在是理解不到那種寫法,爲什麼叫作自執行函數,這裏也順便帶一筆了。
有沒有人,在剛開始接觸到JS
時,會這樣綁定事件:obj.onclick = callback();
而後出錯了卻一直找不到錯誤在哪裏,後來才以後,當一個函數名添加了括號以後,就是函數執行了,那麼也就明白了,上面的寫法,其實就是把callback
函數執行後的返回結果做爲了obj
的click
事件的回調函數了。
而函數名的話,也就是一個function
函數的引用吧,根據函數名查找到對應的function
處理模塊,因此這裏很容易的也就想到了,自執行函數也就是直接在一個匿名函數的後面添加一對小括號,那麼這個匿名函數就會本身執行了。因此也就是自執行函數了。
好比咱們在頁面加載以後,想要當即提示用戶,頁面加載完畢,咱們習慣於這麼寫:
這是咱們經常使用的方法,這裏首先定義個函數,並把函數名命名爲loadSuccess
,以後調用這個函數。很經常使用很簡單。
這裏咱們一般也可使用自執行函數來完成這個提示,你就能夠這樣寫:
完成相同的功能,這裏必須把這個匿名函數放在小括號內部,否則瀏覽器會報錯的。
緣由呢,也是JS
中的常識之一,那就是function A(){}
這樣的定義函數的方法,會在瀏覽器進行預編譯的時候進行解析,而var A = function(){}
這樣的定義函數的方法,則是當JS
解析到該行代碼時,纔會被解析。
這裏呢,若是在上面的自執行函數中,不添加第一個小括號,瀏覽器就會在預編譯時,對該部分進行解析,可是這個時候,由於沒有對這部分function
進行命名,瀏覽器在預編譯時就會報錯,而致使沒法進行下去了。
使用下面這段函數,就能夠證實,是在預編譯的時候,報錯的而致使沒法執行的
固然啦,加括號本就不是必須的,好比咱們使用表達式定義函數時,var A = function(){}
這種寫法,就不是在預編譯的時候進行的,因此,若是咱們的自執行函數會把返回值定義到另一個變量,是能夠省略掉小括號的。
好比:
這樣寫也會連續有兩個alert
執行,完成咱們以前說的功能,也不會報錯,只是這時,自執行函數是沒有返回值的,因此最後的a
變量,是undefined
。不過呢,爲了統一塊兒見,也爲了看着方便,因此仍是對各類寫法的自執行函數的寫法,都添加上小括號吧。
至於爲何,添加了小括號()()
,這樣寫,就能夠,那就是由於,這樣的寫法就變成一個表達式了。。。。
能夠這麼證實一下:
只是這樣的寫法,和表達式定義函數就相似了,並且還會有一個問題就是,A
函數,只有在這個括號內部使用。在外部使用,須要先把這個表達式進行賦值才行,若是賦值,那不就是成了使用賦值表達式定義函數了。
說的遠了點,回來繼續:到這裏也大概瞭解了自執行函數的執行方法了吧。那使用自執行函數的方法,進行事件的綁定,大概也能猜到它的原理了吧。obj.onclick = callback();
。若是我把callback
函數的返回值,定義成一個函數,那當click
事件觸發時,不就是觸發了這個返回的函數了。
因此呢,咱們能夠這樣寫:
沒有什麼問題吧?應該很容易理解到吧。
但是這樣的寫法呢,添加了一個函數變量,若是不添加呢。。。OK的,把後面的函數直接替換過去就好了。。。。
這樣看起來,對比以前的寫法,應該就能很明顯的瞭解到,爲何這麼寫,能獲得咱們想要的結果了吧。
OK,這也是閉包的最簡單的應用了,其餘的閉包寫法也有,只是就原理方面來講,和上面這種是相同的原理,因此這裏就不一一列舉了,用到閉包的地方其實不少(好比惰性載入函數,單例模式中的對象定義等),若是您能理解到這最簡單閉包的原理,那麼其餘用到閉包的地方,見到了,也就能理解了。或者說,想要使用的時候,也就能想到應該怎麼用了吧。
以前的文章中,也有一篇文章中的代碼,主要就是使用的閉包的思想,能夠參考:jQuery源碼學習(二)–proxy
計時器在一些動態頁面,作一些動畫效果時,是不可或缺的一個元素,它和alert
方法相同,都是屬於window
對象的方法。使用計時器時,是有少量差異的,這裏就以setTimeout
爲例簡單說明:
看例子:代碼中中的兩個setTimeout
執行後的結果分別是什麼?
測試一下也就知道了,分別爲1
和2
,由於setTimeout
是把後面執行的方法,第一種寫法,只會查找全局變量中,是否有A
函數,而第二種寫法,會優先查找當前做用域中是否有A
函數,若是局部沒有的話,則順序查找到全局做用域中。
有一種狀況,是說,計時器內部調用的函數的this
指向,是指向window
的,這裏能夠說有錯,也能夠說沒錯,看一個例子:假設給id=test
的一個元素綁定一個click
事件。查看其中的this
的值。
這裏就不考慮在IE8-
的瀏覽器了。
按照最初寫的兩個計時器的例子,在寫出以下的代碼:
爲何?不是按理說,這裏應該是調用的內部的A
方法嗎?爲何this
倒是指向的window
?
有一個不肯定的想法是:當調用了計時器時,會把當前做用域中的方法,內部的this
指向window
對象了。並且僅僅是修改了方法內部的this
指向,若是有私有變量的取值,依然按照原函數所在的位置,根據做用域,進行取值。
能夠這麼證實一下:
this
的指向是和上面一個實例相同的,而alert
中的a
變量的取值,倒是優先獲取局部做用域中的值。
固然啦,這裏若是把計時器中的調用方法,更換一下,那結果就不相同了哦。
這裏,有興趣的能夠試試吧,說到這裏,也發現,雖然使用計時器會強制把調用函數的內部的this
指向改變成指向window
的,可是對於做用域鏈的影響卻只有寫法不一樣帶來的影響。即:setTimeout("A()",1000);
和setTimeout(A,1000);
的不一樣。固然對於第二種寫法,咱們可使用call
和apply
強行改變A
內部this
的指向,不過這些跟本文的內容,貌似沒有什麼關係,就很少說了。
其實,按照我原本的想法,這裏該寫一下計時器(setTimeout,setInterval
)和call
,apply
這幾個和做用域鏈的關係,可是寫到這裏,又感受他們的並無什麼關係,因此關於做用域鏈,就到這裏。
OK了,若是您有什麼新的想法,或者認識,或者發現文中的錯誤,請指教,很是感謝!