到目前爲止,同窗你知道了JavaScript的歷史,也瞭解其「你想是啥就是啥」的變量系統。相信憑藉你深厚的Java或者C++功底,再加上程序員特有的自傲氣質,你確定會信心滿滿:自信寫JavaScript毫無壓力。我也相信寫個Script對於後端攻城師們那確定不在話下。可是,當結果匪夷所想的時候,你或許會一番吐槽:真TM見鬼了,會不會是什麼bug?仍是瀏覽器有問題?個人代碼邏輯沒問題啊......。就像以下代碼,你能說出結果是什麼嗎? javascript
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可以這樣重複定義同名的變量?本樓敢打賭十個看客中,能有一個提出這個疑問,那已是驚喜了。可能有人會說「由於它是弱類型言語」,這個答案只能說對了一半。這看似很不科學、很不嚴謹的變量定義,怎麼可以運行起來呢?很明顯不科學。答案是:有人動了你的代碼!java
有人動了你的代碼!有人動了你的代碼!有人動了你的代碼!重要的事說三遍!那是誰動了你的代碼呢?故事又開始了。node
這事還得回到九十年代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函數作分佈分析。spa
第一步:JavaScript運行時,發現準備要調用func(456)設計
第二步:func是一個函數執行單元,在執行前,須要解析調整code
第三步:爲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=88八、c=1十一、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
各位看官,時間不早了,看看寫得也差很少了。看完這篇博客,你應該知道了我們寫的代碼是被動事後,再運行的。