JavaScript誰動了你的代碼


  到目前為止,同學你知道了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


  各位看官,時間不早了,看看寫得也差不多了。看完這篇博客,你應該知道了咱們寫的代碼是被動過后,再運行的。









免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM