談談javascript語法裡一些難點問題(二)

發表於2014-12-10

3)    作用域鏈相關的問題

作用域鏈是javascript語言裡非常紅的概念,很多學習和使用javascript語言的程式設計師都知道作用域鏈是理解javascript裡很重要的一些概念的關鍵,這些概念包括this指標,閉包等等,它非常紅的另一個重要原因就是作用域鏈理解起來太難,就算有人真的感覺理解了它,但是碰到很多實際問題時候任然會是丈二和尚摸不到頭腦,例如上篇引子裡講到的例子,本篇要講的主題就是作用域鏈,再無別的內容,希望看完本文的朋友能有所收穫。

講作用域鏈首先要從作用域講起,下面是百度百科裡對作用域的定義:

作用域在許多程式設計語言中非常重要。 通常來說,一段程式程式碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的程式碼範圍就是這個名字的作用域。 作用域的使用提高了程式邏輯的區域性性,增強程式的可靠性,減少名字衝突。

在我最擅長的服務端語言java裡也有作用域的概念,java裡作用域是以{}作為邊界,不過在純種的面嚮物件語言裡我們沒必要把作用域研究的那麼深,也沒必要思考複雜的作用域巢狀問題,因為這些語言關於作用域的深度運用並不會給我們編寫的程式碼帶來多大好處。但是在javascript裡卻大不相同,如果我們不能很好的理解javascript的作用域我們就沒辦法使用javascript編寫出複雜的或者規模巨集大的程式。

由百度百科裡的定義,我們知道作用域的作用是保證變數的名字不發生衝突,用現實的場景來理解有個人叫做張三,張三雖然只是一個名字,但是認識張三的人根據名字就能唯一確認這個人到底是誰,但是這個世界上叫做張三的人可不止一個,特別是兩個叫張三的人有交集的時候我們就要有個辦法明確指定這個張三絕不是另外一個張三,這時我們可能會根據兩大張三年齡的差異來區分:例如一個張三叫大張三,相對的另外一個張三叫小張三了。程式語言裡的作用域其實就是為了做類似的標記,作用域會設定一個範圍,在這個範圍裡我們是不會弄錯變數的真實含義。

前面我講到在java裡通過{}來設定作用域,在{}裡面的變數會得到保護,這種保護就是不讓{}裡的變數被外部變數混淆和汙染。那麼{}的方式適合於javascript嗎?我們看看下面的例子:

在javascript世界裡有一個大的作用域環境,這個環境就是window,window環境不需要我們自己使用什麼方式構建,頁面載入時候頁面會自動構造的,上面程式碼裡有一個大括號,這個大括號是對函式的定義,執行之,我們發現函式作用域內部定義的s2變數是不能被window物件訪問的,因此s2變數是被{}保護起來了,它的生命週期和這個函式的生命週期有關。

由這個例子是不是說明在javascript裡,變數也是被{}保護起來了,在javascript語言裡還有非函式的{},我們再看看下面的例子:

我們發現javascript裡{}有時是起不到定義作用域的功能。這也說明javascript裡的作用域定義是和其他語言例如java不同的。

在javascript裡作用域有一個專門的定義execution context,有的書裡把這個名字翻譯成執行上下文,有的書籍裡把它翻譯成執行環境,我更傾向於後者執行環境,下文我提到的執行環境就是execution context。這個命名非常形象,這個形象體現在execution這個單詞,execution含義就是執行,我們來想想javascript裡那些情況是執行:

情況一:當頁面載入時候在script標籤下的javascript程式碼會按順序執行,而這些能被執行的程式碼都是屬於window的變數或函式;

情況二:當函式的名字後面加上小括號(),例如ftn(),這也是在執行,不過它執行的是函式。

如此說來,javascript裡的執行環境有兩類一類是全域性執行環境,即window代表的全域性環境,一類是函式代表的函式執行環境,這也就是我們常說的區域性作用域

執行環境在javascript語言裡並非是一個抽象的概念,而是有具體的實現,這個實現其實是個物件,這個物件也有個名字叫做variable object,這個變數有的書裡翻譯為變數物件,這是直譯,有的書裡把它稱為上下文變數,這裡我還是傾向於後者上下文變數,下文裡提到的上下文變數就是指代variable object。上下文變數儲存的是上下文變數所處執行環境裡定義的所有的變數和函式。

全域性執行環境的上下文變數是可以訪問到的,它就是window物件,所以我們說window能代表全域性作用域是有道理的,但是區域性作用域即函式的執行環境裡的上下文變數是程式碼不能訪問到的,不過javascript引擎在處理資料時候會使用到它。

在javascript語言裡還有一個概念,它的名字叫做execution context stack,翻譯成中文就是執行環境棧,每個要被執行的函式都會先把函式的執行環境壓入到執行環境棧裡,函式執行完畢後,這個函式的執行環境就會被執行環境棧彈出,例如上面的例子:函式執行時候函式的執行環境會被壓入到執行環境棧裡,函式執行完畢,執行環境棧會把這個環境彈出,執行環境棧的控制權就會交由全域性環境,如果函式後面還有程式碼,那麼程式碼就是接著執行。如果函式裡巢狀了函式,那麼巢狀函式執行完畢後,執行環境棧的控制權就交由了外部函式,然後依次類推,最後就是全域性執行環境了。

講到這裡我們大名鼎鼎的作用域鏈要登場了,函式的執行環境被壓入到執行環境棧裡後,函式就要執行了,函式執行的第一步不是執行函式裡的第一行程式碼而是在上下文變數裡構造一個作用域鏈,作用域鏈的英文名字叫做scope chain,作用域鏈的作用是保證執行環境裡有權訪問的變數和函式是有序的,這個概念裡有兩個關鍵意思:有權訪問和有序,我們看看下面的程式碼:

有這個例子我們發現,ftn2函式可以訪問變數b1,b2,這個體現了有權訪問的概念,當ftn1作用域裡改變了b1的值並且把b1變數重新定義為ftn1的區域性變數,那麼ftn2訪問到的b1就是ftn1的,ftn2訪問到b1後就不會在全域性作用域裡查詢b1了,這個體現了有序性。

下面我要總結下上面講述的知識:

本篇的小標題是:作用域鏈的相關問題,這個標題定義的含義是指作用域鏈是大名鼎鼎了,但是作用域鏈在廣大程式設計師的理解裡其實包含的意義已經超越了作用域鏈在javascript語言本身的定義。廣大程式設計師對作用域鏈的理解有兩塊一塊是作用域,而作用域在javascript語言裡指的是執行環境execution context,執行環境在javascript引擎裡是通過上下文變數體現的variable object,javascript引擎裡還有一個概念就是執行環境棧execution context stack,當某一個函式的執行環境壓入到了執行環境棧裡,這個時候就會在上下文變數裡構造一個物件,這個物件就是作用域鏈scope chain,而這個作用域鏈就是廣大程式設計師理解的第二塊知識,作用域鏈的作用是保證執行環境裡有權訪問的變數和函式是有序的,作用域鏈的變數只能向上訪問,變數訪問到window物件即被終止,作用域鏈向下訪問變數是不被允許的。

很多人常常認為作用域鏈是理解this指標的關鍵,這個理解是不正確的的,this指標構造是和作用域鏈同時發生的,也就是說在上文變數構建作用域鏈的同時還會構造一個this物件,this物件也是屬於上下文變數,this變數的值就是當前執行環境外部的上下文變數的一份拷貝,這個拷貝里是沒有作用域鏈變數的,例如程式碼:

我們看到函式ftn1和ftn2裡的this指標都是指向window,這是為什麼了?因為在javascript我們定義函式方式是通過function xxx(){}形式,那麼這個函式不管定義在哪裡,它都屬於全域性物件window,所以他們的執行環境的外部的執行上下文都是指向window。

但是我們都知道現實程式碼很多this指標都不是指向window,例如下面的程式碼:

執行之,我們發現這裡this指標指向了Object,這就怪了我前文不是說javascript裡作用域只有兩種型別:一個是全域性的一個是函式,為什麼這裡Object也是可以製造出作用域了,那麼我的理論是不是有問題啊?那我們看看下面的程式碼:

這兩種寫法是等價的,第一種物件的定義方法叫做字面量定義,而第二種寫法則是標準寫法,Object物件的本質也是個function,所以當我們呼叫物件裡的函式時候,函式的外部執行環境就是obj1本身,即外部執行環境上下文變數代表的就是obj1,那麼this指標也是指向了obj1。

哦,11點了,明天要上班,今天就寫到這裡,關於作用域鏈還有執行環境以及this的關係還有點沒講完,它們的關係會牽涉new的使用,(下面文字我要加粗,因為本文未講this與new的關係,因此this的結論還不完整)寫起來內容很多,所以這些內容就放在本系列的第三篇吧。

相關文章