到目前為止,同學你知道了JavaScript的歷史,也瞭解其“你想是啥就是啥”的變數系統。相信憑藉你深厚的Java或者C++功底,再加上程式設計師特有的自傲氣質,你肯定會信心滿滿:自信寫JavaScript毫無壓力。我也相信寫個Script對於後端攻城師們那肯定不在話下。但是,當結果匪夷所想的時候,你或許會一番吐槽:真TM見鬼了,會不會是什麼bug?還是瀏覽器有問題?我的程式碼邏輯沒問題啊......。就像如下程式碼,你能說出結果是什麼嗎?
var a=123; var b=999; function func(a){ var b; console.log(a);//?????? 結果是什麼????留著分析 var a=888; c=1111;function a(){ } console.log(a);//?????? 結果是什麼????留著分析 console.log(b);//?????? 結果是什麼????留著分析 console.log(c);//?????? 結果是什麼????留著分析 } func(456);
是的,你的程式碼沒有問題,當然瀏覽器也沒問題。你或許說我才不會寫得滿屏都是“a”的程式碼!講真的,當你看到這段程式碼的時候,你有沒有想過為什麼JavaScript能夠這樣重複定義同名的變數?本樓敢打賭十個看客中,能有一個提出這個疑問,那已經是驚喜了。可能有人會說“因為它是弱型別言語”,這個答案只能說對了一半。這看似很不科學、很不嚴謹的變數定義,怎麼能夠執行起來呢?很明顯不科學。答案是:有人動了你的程式碼!
有人動了你的程式碼!有人動了你的程式碼!有人動了你的程式碼!重要的事說三遍!那是誰動了你的程式碼呢?故事又開始了。
這事還得回到九十年代JavaScript出生那會。話說布蘭登-艾奇當時創造JavaScript的時候,他的需求就是做做客戶端的資料驗證而已。於是乎,他想“這玩兒沒必要搞高能設計,看上去好像也沒有什麼地方需要高能運算的,搞預編譯、連結器那是太浪費了,再說這玩兒是在瀏覽器上跑的,搞編譯器、連結器,那瀏覽器不成了IDE啦?最好能像Perl那樣,邊解析邊執行最美不過”。鞋同們看到這裡應該明白了:那麼多廢話,你就不是為了說JavaScript是邊解析邊執行的嘛!我懂的,這個課本上有說。但是好多課本好像只說了邊解析邊執行,但是沒說是怎麼解析的,就算有說了,那也是廢話比這篇博文還多,還說不清楚。到此,前面高呼三聲那個問題的答案,想必看官到此也看出答案了:解析器動了你的程式碼!
解析器動了你的程式碼!那得先認認真真說下“從你敲下程式碼,然後執行,最後輸出結果”這個過程到底發生了什麼?課本都說了“邊解析、邊執行”,毫無疑問這個過程就分為“解析期”與“執行期”。那下面我們就以上面的程式碼為例,看看你的程式碼是怎麼被動了手腳後再執行的。
解析期
先照本宣科說說樓主對解析期的理解:解析期就是每一個執行單元在程式碼執行前,解析器對使用者程式碼(程式設計師寫的程式碼)進行解析調整的時期。這裡有個關鍵的術語“執行單元”。什麼是執行單元?這裡僅以瀏覽器環境做說明(nodejs環境可能不一樣)。簡單地理解,一個頁面是一個執行單元,一個function也是一個執行單元。一個頁面的JavaScript在執行前,頁面的所有JavaScript宣告定義都被解析調整一遍;在一個function在執行前,這個function內的所有JavaScript宣告定義(包括形參)都被解析調整了一遍。看了本樓的個人見解(如有誤,請斧正),你或許會問:按你的意思頁面載入完成的時候,先解析了一次頁面上的JavaScript,之後在呼叫function的時候又進行了一次解析,那豈不是有n次解析?對!沒錯,有n次解析!鞋同你看準了,樓主特意高亮的【JavaScript宣告定義】。那什麼是宣告定義呢?且看程式碼:
var a;//是宣告定義 var a=123;//包含了宣告定義、賦值運算表示式 function f(){//是一個function定義 } var f=function(){//包含了宣告定義、function賦值運算表示式 }
看官要是有耐心看到這裡,你應該明白了什麼是解析期,也瞭解了什麼是JavaScript宣告定義。本樓再次強調“解析器只對宣告定義”進行解析調整,像上面的“var a=123”、“var f=function(){}”會被拆為兩部分,宣告定義及賦值運算!宣告定義用於解析期,賦值執行用於執行期。那解析器是怎麼解析調整JavaScript的宣告定義的呢?下面以博文第一段給出疑問的示列程式碼func函式做分佈分析。
第一步:JavaScript執行時,發現準備要呼叫func(456)
第二步:func是一個函式執行單元,在執行前,需要解析調整
第三步:為func執行單元準備一個當前的ActivityObject活動物件,即在func執行單元內生成一個所謂的活動物件,虛擬碼為:var AO={};
第四步:先解析func形參定義,發現func定義了一個形參a,那麼將a掛到AO物件上,並且將實參賦給形參,AO={a:456}
第五步:解析變數宣告定義,發現定義了var b,AO={a:456,b:undefined}
第六步:解析變數宣告定義var a=888,拆分為var a;a=888;發現AO中已經有了a定義,不做調整,AO={a:456,b:undefined}
第七步:解析函式定義,發現function a(){}函式定義,AO={a:function(){},b:undefined}
怎麼樣!看官,知道解析器是怎麼動了你的程式碼吧。你寫的所有宣告定義都被移動到了一個活動物件上!請記住,解析器是這樣動你的程式碼的:準備活動物件,然後解析形參而且進行實參賦值,然後解析函式內的var 變數宣告定義(如果包含賦值則拆分賦值運算)、然後再解析函式定義。
到目前為止,解析器偷樑換柱的工作做完了,一切就緒,只欠Running!那Running什麼?剩下的那些程式碼就是Running的,如var a=888、c=111、console.log()。就是執行期裡面要發生的事情。那接下來,說說執行期的事情,結果便會分曉!
執行期
執行期,那就是直接跑程式碼咯,沒什麼定義好說的。但是這個執行期還有個令人驚訝的地方。這傢伙每遇到一個變數(包括函式變數),都會先從當前的ActivityObject中查詢是否存在,如果不存在則往上查詢(作用鏈?原型鏈?這裡預留下一篇博文)。這個奇怪的行為就造成了前面博文提到的神奇的變數提升作用。看官,你終於知道什麼是變數提升了吧,也知道變數提升是什麼鬼造成的了吧!好!廢話少說,我們們還是規矩分析下執行期是怎麼跑程式碼的。
第一步:執行console.log(a),找AO物件,發現a=function,所以第一個結果是function(){}
第二步:執行var a=888,找AO物件,發現有個a定義,執行賦值運算,此時AO={a:888,b:undefined},函式被覆蓋了!
第三步:執行c=1111,找AO物件,沒貨!往上找,還是沒貨,好吧,到處沒貨,那隻能留給父親大人了,於是c變成了父親大人的成員,並賦值為1111
第四步:執行console.log(a),找AO物件,發現有料,a=888,結果是888
第五步:執行console.log(b),找AO物件,發現有料,b=undefined,結果undefined,特別宣告:undefined和xxx is not defined是兩回事!
第六步:執行console.log(c),找AO物件,沒貨,找父親大人的,發現父親大人有個c=1111,結果是1111
各位看官,時間不早了,看看寫得也差不多了。看完這篇部落格,你應該知道了我們們寫的程式碼是被動過後,再執行的。