3) 作用域鏈相關的問題
作用域鏈是javascript語言里非常紅的概念,很多學習和使用javascript語言的程序員都知道作用域鏈是理解javascript里很重要的一些概念的關鍵,這些概念包括this指針,閉包等等,它非常紅的另一個重要原因就是作用域鏈理解起來太難,就算有人真的感覺理解了它,但是碰到很多實際問題時候任然會是丈二和尚摸不到頭腦,例如上篇引子里講到的例子,本篇要講的主題就是作用域鏈,再無別的內容,希望看完本文的朋友能有所收獲。
講作用域鏈首先要從作用域講起,下面是百度百科里對作用域的定義:
作用域在許多程序設計語言中非常重要。 通常來說,一段程序代碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的代碼范圍就是這個名字的作用域。 作用域的使用提高了程序邏輯的局部性,增強程序的可靠性,減少名字沖突。
在我最擅長的服務端語言java里也有作用域的概念,java里作用域是以{}作為邊界,不過在純種的面向對象語言里我們沒必要把作用域研究的那么深,也沒必要思考復雜的作用域嵌套問題,因為這些語言關於作用域的深度運用並不會給我們編寫的代碼帶來多大好處。但是在javascript里卻大不相同,如果我們不能很好的理解javascript的作用域我們就沒辦法使用javascript編寫出復雜的或者規模宏大的程序。
由百度百科里的定義,我們知道作用域的作用是保證變量的名字不發生沖突,用現實的場景來理解有個人叫做張三,張三雖然只是一個名字,但是認識張三的人根據名字就能唯一確認這個人到底是誰,但是這個世界上叫做張三的人可不止一個,特別是兩個叫張三的人有交集的時候我們就要有個辦法明確指定這個張三絕不是另外一個張三,這時我們可能會根據兩大張三年齡的差異來區分:例如一個張三叫大張三,相對的另外一個張三叫小張三了。編程語言里的作用域其實就是為了做類似的標記,作用域會設定一個范圍,在這個范圍里我們是不會弄錯變量的真實含義。
前面我講到在java里通過{}來設置作用域,在{}里面的變量會得到保護,這種保護就是不讓{}里的變量被外部變量混淆和污染。那么{}的方式適合於javascript嗎?我們看看下面的例子:
var s1 = "sharpxiajun"; function ftn(){ var s2 = "xtq"; console.log(this);// 運行結果: window console.log("s1:" + this.s1 + ";s2:" + this.s2);//運行結果:s1:sharpxiajun;s2:undefined console.log("s1:" + this.s1 + ";s2:" + s2);// 運行結果:s1:sharpxiajun;s2:xtq } ftn();
在javascript世界里有一個大的作用域環境,這個環境就是window,window環境不需要我們自己使用什么方式構建,頁面加載時候頁面會自動構造的,上面代碼里有一個大括號,這個大括號是對函數的定義,運行之,我們發現函數作用域內部定義的s2變量是不能被window對象訪問的,因此s2變量是被{}保護起來了,它的生命周期和這個函數的生命周期有關。
由這個例子是不是說明在javascript里,變量也是被{}保護起來了,在javascript語言里還有非函數的{},我們再看看下面的例子:
if (true){ var a = "aaaa"; } console.log(a);// 運行結果:aaaa
我們發現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,作用域鏈的作用是保證執行環境里有權訪問的變量和函數是有序的,這個概念里有兩個關鍵意思:有權訪問和有序,我們看看下面的代碼:
var b1 = "b1"; function ftn1(){ var b2 = "b2"; var b1 = "bbb"; function ftn2(){ var b3 = "b3"; b2 = b1; b1 = b3; console.log("b1:" + b1 + ";b2:" + b2 + ";b3:" + b3);// 運行結果:b1:b3;b2:bbb;b3:b3 } ftn2(); } ftn1(); console.log(b1);// 運行結果:b1
有這個例子我們發現,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變量的值就是當前執行環境外部的上下文變量的一份拷貝,這個拷貝里是沒有作用域鏈變量的,例如代碼:
var b1 = "b1"; function ftn1(){ console.log(this);// 運行結果: window var b2 = "b2"; var b1 = "bbb"; function ftn2(){ console.log(this);// 運行結果: window var b3 = "b3"; b2 = b1; b1 = b3; console.log("b1:" + b1 + ";b2:" + b2 + ";b3:" + b3);// 運行結果:b1:b3;b2:bbb;b3:b3 } ftn2(); } ftn1();
我們看到函數ftn1和ftn2里的this指針都是指向window,這是為什么了?因為在javascript我們定義函數方式是通過function xxx(){}形式,那么這個函數不管定義在哪里,它都屬於全局對象window,所以他們的執行環境的外部的執行上下文都是指向window。
但是我們都知道現實代碼很多this指針都不是指向window,例如下面的代碼:
var obj = { name:"sharpxiajun", ftn:function(){ console.log(this);// 運行結果: Object { name="sharpxiajun", ftn=function()} console.log(this.name);//運行結果: sharpxiajun } } obj.ftn();// :
運行之,我們發現這里this指針指向了Object,這就怪了我前文不是說javascript里作用域只有兩種類型:一個是全局的一個是函數,為什么這里Object也是可以制造出作用域了,那么我的理論是不是有問題啊?那我們看看下面的代碼:
var obj1 = new Object(); obj1.name = "xtq"; obj1.ftn = function(){ console.log(this);// 運行結果: Object { name="xtq", ftn=function()} console.log(this.name);//運行結果: xtq } obj1.ftn();
這兩種寫法是等價的,第一種對象的定義方法叫做字面量定義,而第二種寫法則是標准寫法,Object對象的本質也是個function,所以當我們調用對象里的函數時候,函數的外部執行環境就是obj1本身,即外部執行環境上下文變量代表的就是obj1,那么this指針也是指向了obj1。
哦,11點了,明天要上班,今天就寫到這里,關於作用域鏈還有執行環境以及this的關系還有點沒講完,它們的關系會牽涉new的使用,(下面文字我要加粗,因為本文未講this與new的關系,因此this的結論還不完整)寫起來內容很多,所以這些內容就放在本系列的第三篇吧。
最后祝大家晚安。