天天看點

至簡·作用域及作用域鍊

說起作用域,先說說作用域的類型吧,對于不同的語言,它們的作用域類型也許不同,主要分為詞法作用域(靜态作用域)和動态作用域。

  • 詞法作用域是在函數定義時就已确定。也就是在我們書寫代碼時,就已經确定了,然後在我們執行函數時,再遇到既不是形參也不是函數内部定義的局部變量的變量時,就會到我們定義時的環境中尋找所需的變量。
  • 動态作用域是在函數調用時才确定的。也就是說再遇到既不是形參也不是函數内部定義的局部變量的變量時,函數在哪裡調用,就在那裡尋找所需的變量。它與變量的值,與它定義在哪裡無關,與它在哪裡調用有關。

說實話動态作用域,我是基本沒用過,也許用過也忘了。以一張圖為解釋。

至簡·作用域及作用域鍊

好了,簡單的了解了什麼是詞法作用域以及動态作用域,我們就開始針對javascript進行分析。先說說我們開篇提到的問題,什麼是作用域?

之前對于這個知識點,的确屬于霧裡看花的那種感覺,,在網上基本都是一個了解方式,就是function内部就是作用域。(O_o) ?? 你當我胖虎好糊弄是不是。不過說實話,這話,也沒錯。但是肯定沒這麼簡單啊。通過我胖虎不斷的翻閱,個人呢也産生了一些小小的了解。一下就是我對作用域的了解。

個人了解:作用域其實就是聲明變量時,所在的區域(包括變量聲明和函數聲明)。這片區域儲存了目前環境下的執行期上下文。

我們在聲明變量,或者聲明一個函數時,其實就存在于一個環境中。如果是在一個函數内部,那麼也就是這個函數就是目前所處的作用域,如果是在全局中聲明變量,那麼所處的就是全局作用域。也許你已經猜到,那麼就是有變量作用域和函數作用域兩種喽,對不起,我們本次僅讨論ES6之前的版本,咱先不提let,好吧!對于ES5之前,隻有函數作用域。也就是說每當聲明一個函數,也就建立了一塊作用域。而這塊作用域儲存着的也正是目前環境中的執行期上下文。這一點也不難了解,處于函數内部,而函數執行時,就會生成執行期上下文,而作用域正是儲存的這個環境,那也就是這片執行期上下文。

那麼作用域是在哪裡儲存的呢,在函數有一個内部屬性,即[[scope]]屬性,在函數建立之時,就會儲存所有的外部變量到其中,這是因為js是詞法作用域的緣故。是以如果存在多層函數嵌套,就會産生一個儲存外部對象的層級鍊。當函數運作,查找變量時,就會從目前儲存的執行期上下文中查找,如果沒有,就會繼續查找所目前儲存的執行期上下文所處的作用域,也就是向詞法層面上的父級作用域查找,一直向外找,向外找,直到找到全局的執行期上下文中。像這樣的有多級上下文變量對象構成的這種鍊式結構,也就是作用域鍊的概念。

說的再多,不如舉個栗子:

function foo(){
	function bar(){}
}
           

我們來觀察一下這兩個函數,分别所處的作用域是什麼:

foo.[[scope]] = {
	gloabelContext.VO//全局作用域的執行期上下文
},
bar.[[scope]]={
	fooContext.AO,//外部的foo函數
	gloabelContext.VO //更外層的全局執行期上下文
}
           

以上這種的函數作用域是函數還未執行時的作用域,函數被建立時會将其所處的作用域儲存到自身的[[scope]]屬性中。當函數激活時,進入函數的執行期上下文在函數執行的前一刻,函數被激活。要發生幾件很重要的事,函數會建構完整的作用域鍊,也就是将自身也要加入其中。步驟如下:

建立活動對象,然後将活動對象添加到自身作用域鍊的最頂端,最終foo所儲存的作用域鍊就是形如 Scope:[AO,[[scope]]],這樣的一個作用域鍊,最前面的,也就是作用域鍊的最頂端的,是目前函數的執行期上下文内的活動對象AO,并且還攜帶着上一層的作用域,像一條鍊子。

我們來具體分析分析,這條鍊是怎麼生成的。

  1. 函數foo被建立,儲存所處的作用域鍊,到其内部屬性[[scope]]中
foo.[[scope]] = [
	globalContext.VO
]
//作用域鍊所處的環境是一個棧結構,為了友善于了解,暫且以數組的形式呈現
           
  1. 執行foo函數的前一刻,産生對應的執行期上下文,然後将所産生的執行期上下文壓入ESC執行棧中
ESC=[
	checkscopeContext,
	gloabelContext
]
           

3.foo函數執行前,不立即執行,開始準備工作。

  • 第一步:複制自身的[[scope]]屬性,準備建立作用域鍊
fooContext{
	scope:foo.[[scope]],
}
           
  • 第二步:函數被激活,建立foo的活動對象AO,即各種形參、函數、變量的聲明,并被arguments對象初始化。
fooContext{
	AO:{
		arguments:{
			length:0
		},
		......
		bar:function bar{}
	}
	scope:foo.[[scope]]
}
           
  • 第三步:将活動對象AO壓入作用域鍊鍊的最頂端。
fooContext{
	AO:{
		arguments:{
			length:0
		},
		......
		bar:function bar{}
	},
	scope:[AO,[[scope]]]  
	//這裡儲存的就是自身的AO,以及foo所處的作用域(全局作用域),生成完整的作用域鍊
	//寫成上面那樣可能不太好了解,這樣寫 scope:[ AO , foo.[[scope]] ] ,這樣就很明顯了
}
           

至此準備工作結束,開始函數的執行,即操作AO中的屬性值,要查找變量就會先查找自身的AO中是否有,如果沒有就會沿着儲存的作用域鍊向上查找,直到全局作用域中。最終代碼執行結束,執行期foo的上下文從ESC中彈出,被回收。

ESC=[
	globalContext
]
           

在這個環節中我沒有分析bar函數的執行過程,因為bar函數執行的過程以及作用域鍊的産生原理與foo的是一樣的,

可以直接給出一個結果:

barContext:{
	AO:{
		length:0
	},
	scope:[ AO,bar.[[scope]],foo.[[scope]] ]  
	//bar自身AO,bar所處作用域也就是 scope:[ AO , bar.[[scope]] ] (foo的),foo.[[scope]](全局的)
	//可以看到bar的作用域鍊中儲存着自身的AO,以及foo的AO和foo儲存的全局上下文,是以就可以沿着整個作用域鍊向上查找所需的變量。
}
           

再貼上一幅圖,以便于了解

至簡·作用域及作用域鍊

   最後再舉一個例子,這個作用域鍊,其實就類似于一個家族的财富鍊,爺爺輩的人自己打拼下的财富就類似于全局作用域,父輩,自出生的那一刻就身處于爺爺輩打拼下的财富中,其次父輩又會産生自身的财富,父輩有需要,先找自己的,自己沒有了,可以向父輩要。然後再到子輩,子輩自出生的那一刻,即直接身處于父輩打拼下的财富中,其次也能擷取,爺爺輩的财富,最終,子輩也将創造自己的财富,像這樣構成的這種财富的層級鍊,與作用域鍊的方式基本一緻。

繼續閱讀