詳解JavaScript中的作用域鏈與閉包
作用域鏈
首先來看看這段代碼:
var a = '喜羊羊'; function A(){ console.log(a); a = '美羊羊'; function B(){ console.log(a); } B(); } A();
在這里毫無疑問結(jié)果肯定是我們想到的先打印喜羊羊,再打印美羊羊。因為作用域鏈嘛,如果當(dāng)前層沒找到,那么就去當(dāng)前層的上一級找。
那么再看這道
function bar() { console.log(myName) } function foo() { var myName = "極客邦" bar() } var myName = "極客時間" foo()
是不是感覺是打印極客邦?如果是的話,那么恭喜你,掉坑里了。(還不趕快爬起來,補一補作用域鏈的知識)。
為什么打印不是極客邦而是極客時間呢?
既然問題出現(xiàn)在了對作用域鏈的理解上,那么就再回到作用域鏈的定義上吧。
其實在每個執(zhí)行上下文的變量環(huán)境中,都包含了一個外部引用,用來指向外部的執(zhí)行上下文,我們把這個外部引用稱為 outer。
比如上面那段代碼在查找 myName
變量時,如果在當(dāng)前的變量環(huán)境中沒有查找到,那么 JavaScript 引擎會繼續(xù)在 outer 所指向的執(zhí)行上下文中查找
為了直觀理解,你可以看下面這張圖:
看到這張圖我猜你又納悶了,為什么bar
函數(shù)創(chuàng)建的執(zhí)行上下文中的outer會指向全局??
哈哈哈,這里就要涉及到了詞法作用域了
詞法作用域
詞法作用域就是指作用域是由代碼中函數(shù)聲明的位置來決定的,所以詞法作用域是靜態(tài)的作用域,通過它就能夠預(yù)測代碼在執(zhí)行過程中如何查找標識符。
這么講可能不太好理解,你可以看下面這張圖:
從圖中可以看出,詞法作用域就是根據(jù)代碼的位置來決定的,其中 main
函數(shù)包含了 bar
函數(shù),bar
函數(shù)中包含了 foo
函數(shù),因為 JavaScript 作用域鏈是由詞法作用域決定的,所以整個詞法作用域鏈的順序是:foo 函數(shù)作用域—>bar 函數(shù)作用域—>main 函數(shù)作用域—> 全局作用域。
明白了詞法作用域,那么我們再回到剛剛的問題。
為什么bar函數(shù)創(chuàng)建的執(zhí)行上下文中的outer會指向全局
這是因為根據(jù)詞法作用域,而詞法作用域又是根據(jù)代碼的位置,而bar函數(shù)代碼的位置就是包裹在全局下,而喜羊羊那個例子中的B函數(shù)是在A函數(shù)的環(huán)境下,所以會造成它們的詞法作用域鏈不同,也就導(dǎo)致函數(shù)作用域鏈不同了。
所以我們才有那句話詞法作用域是代碼編譯階段就決定好的,和函數(shù)是怎么調(diào)用的沒有關(guān)系。
也就是只和代碼位置有關(guān),和函數(shù)直接如何調(diào)用沒關(guān)系
閉包
老生常談的問題,這次再從一個更深入的角度來理解一下。
看下面這段代碼:
function foo() { var myName = "極客時間" let test1 = 1 const test2 = 2 var innerBar = { getName:function(){ console.log(test1) return myName }, setName:function(newName){ myName = newName } } return innerBar } var bar = foo() bar.setName("極客邦") bar.getName() console.log(bar.getName())
這段代碼乍一看沒有什么問題,但是這里有一個細節(jié)很多人會忽視。
在foo()執(zhí)行完將返回值給bar
時,這里foo函數(shù)會從調(diào)用棧中彈出,變量都會被回收。既然變量都被回收了,那么bar.setName()
這些調(diào)用方法從何而來??
foo執(zhí)行完后的情況可以參考下圖:
從上圖可以看出,foo
函數(shù)執(zhí)行完成之后,其執(zhí)行上下文從棧頂彈出了,但是由于返回的 setName
和 getName
方法中使用了 foo 函數(shù)內(nèi)部的變量 myName
和 test1
,所以這兩個變量依然保存在內(nèi)存中。這像極了 setName
和 getName
方法背的一個專屬背包,無論在哪里調(diào)用了 setName
和 getName
方法,它們都會背著這個foo
函數(shù)的專屬背包。
之所以是專屬背包,是因為除了 setName
和 getName
函數(shù)之外,其他任何地方都是無法訪問該背包的,我們就可以把這個背包稱為 foo 函數(shù)的閉包。
好了,現(xiàn)在我們終于可以給閉包一個正式的定義了。在 JavaScript 中,根據(jù)詞法作用域的規(guī)則,內(nèi)部函數(shù)總是可以訪問其外部函數(shù)中聲明的變量,當(dāng)通過調(diào)用一個外部函數(shù)返回一個內(nèi)部函數(shù)后,即使該外部函數(shù)已經(jīng)執(zhí)行結(jié)束了,但是內(nèi)部函數(shù)引用外部函數(shù)的變量依然保存在內(nèi)存中,我們就把這些變量的集合稱為閉包 比如外部函數(shù)是 foo,那么這些變量的集合就稱為 foo 函數(shù)的閉包。
用一句話概括就是
能夠訪問其他函數(shù)內(nèi)部變量的函數(shù),被稱為 閉包。
(我們理解可以這么理解,但是和面試官說的當(dāng)然可以把這個例子說一下,這直接上升到了一個理解什么是閉包的新高度了)
到此這篇關(guān)于詳解JavaScript中的作用域鏈與閉包的文章就介紹到這了,更多相關(guān)JavaScript作用域鏈 閉包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript創(chuàng)建對象的方式小結(jié)(4種方式)
這篇文章主要介紹了JavaScript創(chuàng)建對象的方式,結(jié)合實例形式總結(jié)分析了四種創(chuàng)建對象的方式,并附帶分析了JavaScript對象復(fù)制的技巧,需要的朋友可以參考下2015-12-12原生javascript實現(xiàn)DIV拖拽并計算重復(fù)面積
這篇文章主要介紹了使用原生javascript實現(xiàn)DIV拖拽并計算重復(fù)面積的方法及示例代碼分享,效果十分漂亮,需要的朋友可以參考下2015-01-01編寫更好的JavaScript條件式和匹配條件的技巧(小結(jié))
這篇文章主要介紹了編寫更好的JavaScript條件式和匹配條件的技巧(小結(jié)),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06