官术网_书友最值得收藏!

1.2 理解作用域

我們學(xué)習(xí)作用域的方式是將這個過程模擬成幾個人物之間的對話。那么,由進行這場對話呢?

1.2.1 演員表

首先介紹將要參與到對程序var a = 2;進行處理的過程中的演員們,這樣才能理解接下來將要聽到的對話。

· 引擎

從頭到尾負(fù)責(zé)整個JavaScript程序的編譯及執(zhí)行過程。

· 編譯器

引擎的好朋友之一,負(fù)責(zé)語法分析及代碼生成等臟活累活(詳見前一節(jié)的內(nèi)容)。

· 作用域

引擎的另一位好朋友,負(fù)責(zé)收集并維護由所有聲明的標(biāo)識符(變量)組成的一系列查詢,并實施一套非常嚴(yán)格的規(guī)則,確定當(dāng)前執(zhí)行的代碼對這些標(biāo)識符的訪問權(quán)限。

為了能夠完全理解JavaScript的工作原理,你需要開始像引擎(和它的朋友們)一樣思考,從它們的角度提出問題,并從它們的角度回答這些問題。

1.2.2 對話

當(dāng)你看見var a = 2;這段程序時,很可能認(rèn)為這是一句聲明。但我們的新朋友引擎卻不這么看。事實上,引擎認(rèn)為這里有兩個完全不同的聲明,一個由編譯器在編譯時處理,另一個則由引擎在運行時處理。

下面我們將var a = 2;分解,看看引擎和它的朋友們是如何協(xié)同工作的。

編譯器首先會將這段程序分解成詞法單元,然后將詞法單元解析成一個樹結(jié)構(gòu)。但是當(dāng)編譯器開始進行代碼生成時,它對這段程序的處理方式會和預(yù)期的有所不同。

可以合理地假設(shè)編譯器所產(chǎn)生的代碼能夠用下面的偽代碼進行概括:“為一個變量分配內(nèi)存,將其命名為a,然后將值2保存進這個變量。”然而,這并不完全正確。

事實上編譯器會進行如下處理。

1.遇到var a,編譯器會詢問作用域是否已經(jīng)有一個該名稱的變量存在于同一個作用域的集合中。如果是,編譯器會忽略該聲明,繼續(xù)進行編譯;否則它會要求作用域在當(dāng)前作用域的集合中聲明一個新的變量,并命名為a。

2.接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理a = 2這個賦值操作。引擎運行時會首先詢問作用域,在當(dāng)前的作用域集合中是否存在一個叫作a的變量。如果是,引擎就會使用這個變量;如果否,引擎會繼續(xù)查找該變量(查看1.3節(jié))。

如果引擎最終找到了a變量,就會將2賦值給它。否則引擎就會舉手示意并拋出一個異常!

總結(jié):變量的賦值操作會執(zhí)行兩個動作,首先編譯器會在當(dāng)前作用域中聲明一個變量(如果之前沒有聲明過),然后在運行時引擎會在作用域中查找該變量,如果能夠找到就會對它賦值。

1.2.3 編譯器有話說

為了進一步理解,我們需要多介紹一點編譯器的術(shù)語。

編譯器在編譯過程的第二步中生成了代碼,引擎執(zhí)行它時,會通過查找變量a來判斷它是否已聲明過。查找的過程由作用域進行協(xié)助,但是引擎執(zhí)行怎樣的查找,會影響最終的查找結(jié)果。

在我們的例子中,引擎會為變量a進行LHS查詢。另外一個查找的類型叫作RHS。

我打賭你一定能猜到“L”和“R”的含義,它們分別代表左側(cè)和右側(cè)。

什么東西的左側(cè)和右側(cè)?是一個賦值操作的左側(cè)和右側(cè)。

換句話說,當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時進行LHS查詢,出現(xiàn)在右側(cè)時進行RHS查詢。

講得更準(zhǔn)確一點,RHS查詢與簡單地查找某個變量的值別無二致,而LHS查詢則是試圖找到變量的容器本身,從而可以對其賦值。從這個角度說,RHS并不是真正意義上的“賦值操作的右側(cè)”,更準(zhǔn)確地說是“非左側(cè)”。

你可以將RHS理解成retrieve his source value(取到它的源值),這意味著“得到某某的值”。

讓我們繼續(xù)深入研究。

考慮以下代碼:

        console.log(a);

其中對a的引用是一個RHS引用,因為這里a并沒有賦予任何值。相應(yīng)地,需要查找并取得a的值,這樣才能將值傳遞給console.log(..)。

相比之下,例如:

        a = 2;

這里對a的引用則是LHS引用,因為實際上我們并不關(guān)心當(dāng)前的值是什么,只是想要為=2這個賦值操作找到一個目標(biāo)。

LHS和RHS的含義是“賦值操作的左側(cè)或右側(cè)”并不一定意味著就是“=賦值操作符的左側(cè)或右側(cè)”。賦值操作還有其他幾種形式,因此在概念上最好將其理解為“賦值操作的目標(biāo)是誰(LHS)”以及“誰是賦值操作的源頭(RHS)”。

考慮下面的程序,其中既有LHS也有RHS引用:

        function foo (a) {
            console.log (a); // 2
        }
        foo (2);

最后一行foo (..)函數(shù)的調(diào)用需要對foo進行RHS引用,意味著“去找到foo的值,并把它給我”。并且(..)意味著foo的值需要被執(zhí)行,因此它最好真的是一個函數(shù)類型的值!

這里還有一個容易被忽略卻非常重要的細(xì)節(jié)。

代碼中隱式的a=2操作可能很容易被你忽略掉。這個操作發(fā)生在2被當(dāng)作參數(shù)傳遞給foo(..)函數(shù)時,2會被分配給參數(shù)a。為了給參數(shù)a(隱式地)分配值,需要進行一次LHS查詢。

這里還有對a進行的RHS引用,并且將得到的值傳給了console.log(..)。console. log(..)本身也需要一個引用才能執(zhí)行,因此會對console對象進行RHS查詢,并且檢查得到的值中是否有一個叫作log的方法。

最后,在概念上可以理解為在LHS和RHS之間通過對值2進行交互來將其傳遞進log(..)(通過變量a的RHS查詢)。假設(shè)在log(..)函數(shù)的原生實現(xiàn)中它可以接受參數(shù),在將2賦值給其中第一個(也許叫作arg1)參數(shù)之前,這個參數(shù)需要進行LHS引用查詢。

你可能會傾向于將函數(shù)聲明function foo(a) {...概念化為普通的變量聲明和賦值,比如var foo、foo = function(a) {...。如果這樣理解的話,這個函數(shù)聲明將需要進行LHS查詢。

然而還有一個重要的細(xì)微差別,編譯器可以在代碼生成的同時處理聲明和值的定義,比如在引擎執(zhí)行代碼時,并不會有線程專門用來將一個函數(shù)值“分配給”foo。因此,將函數(shù)聲明理解成前面討論的LHS查詢和賦值的形式并不合適。

1.2.4 引擎和作用域的對話

        function foo(a) {
            console.log(a); // 2
        }
        foo(2);

讓我們把上面這段代碼的處理過程想象成一段對話,這段對話可能是下面這樣的。

引擎:我說作用域,我需要為foo進行RHS引用。你見過它嗎?

作用域:別說,我還真見過,編譯器那小子剛剛聲明了它。它是一個函數(shù),給你。

引擎:哥們太夠意思了!好吧,我來執(zhí)行一下foo。

引擎:作用域,還有個事兒。我需要為a進行LHS引用,這個你見過嗎?

作用域:這個也見過,編譯器最近把它聲名為foo的一個形式參數(shù)了,拿去吧。

引擎:大恩不言謝,你總是這么棒。現(xiàn)在我要把2賦值給a。

引擎:哥們,不好意思又來打擾你。我要為console進行RHS引用,你見過它嗎?

作用域:咱倆誰跟誰啊,再說我就是干這個。這個我也有,console是個內(nèi)置對象。給你。

引擎:么么噠。我得看看這里面是不是有l(wèi)og(..)。太好了,找到了,是一個函數(shù)。

引擎:哥們,能幫我再找一下對a的RHS引用嗎?雖然我記得它,但想再確認(rèn)一次。

作用域:放心吧,這個變量沒有變動過,拿走,不謝。

引擎:真棒。我來把a的值,也就是2,傳遞進log(..)。

……

1.2.5 小測驗

檢驗一下到目前的理解程度。把自己當(dāng)作引擎,并同作用域進行一次“對話”:

        function foo(a) {
            var b = a;
            return a + b;
        }
        var c = foo(2);

1.找到其中所有的LHS查詢。(這里有3處!)

2.找到其中所有的RHS查詢。(這里有4處!)

查看本章小結(jié)中的參考答案。

主站蜘蛛池模板: 岗巴县| 南开区| 喀喇| 冷水江市| 根河市| 湖南省| 綦江县| 同德县| 两当县| 新野县| 靖江市| 迭部县| 怀安县| 临清市| 阳谷县| 瑞昌市| 通州区| 池州市| 香港 | 大城县| 容城县| 右玉县| 芜湖市| 青田县| 新蔡县| 宁陕县| 磐安县| 望都县| 安岳县| 潜江市| 乌拉特前旗| 嫩江县| 巴中市| 东莞市| 高安市| 庆城县| 庆安县| 荆门市| 荣昌县| 渭源县| 寻乌县|