Javascript執行過程詳細研究


下面我們以形象的示例來說明JavaScript代碼在頁面中的執行順序。如果說,JavaScript引擎的工作機制比較深奧是因爲它屬於底層行爲,那麼JavaScript代碼執行順序就比較形象了,因爲我們可以直觀感覺到這種執行順序,當然JavaScript代碼的執行順序是比較複雜的,所以在深入JavaScript語言之前也有必要對其進行剖析。

1.1  按HTML文檔流順序執行JavaScript代碼

首先,讀者應該清楚,HTML文檔在瀏覽器中的解析過程是這樣的:瀏覽器是按着文檔流從上到下逐步解析頁面結構和信息的。JavaScript代碼作爲嵌入的腳本應該也算做HTML文檔的組成部分,所以JavaScript代碼在裝載時的執行順序也是根據腳本標籤<script>的出現順序來確定的。例如,瀏覽下面文檔頁面,你會看到代碼是從上到下逐步被解析的。

[javascript] view plaincopyprint?
  1. <script>  
  2.   
  3. alert("頂部腳本");  
  4.   
  5. </script>  
  6.   
  7. <html><head>  
  8.   
  9. <script>  
  10.   
  11. alert("頭部腳本");  
  12.   
  13. </script>  
  14.   
  15. <title></title>  
  16.   
  17. </head>  
  18.   
  19. <body>  
  20.   
  21. <script>  
  22.   
  23. alert("頁面腳本");  
  24.   
  25. </script>  
  26.   
  27. </body></html>  
  28.   
  29. <script>  
  30.   
  31. alert("底部腳本");  
  32.   
  33. </script>  


如果通過腳本標籤<script>的src屬性導入外部JavaScript文件腳本,那麼它也將按照其語句出現的順序來執行,而且執行過程是文檔裝載的一部分。不會因爲是外部JavaScript文件而延期執行。例如,把上面文檔中的頭部和主體區域的腳本移到外部JavaScript文件中,然後通過src屬性導入。繼續預覽頁面文檔,你會看到相同的執行順序。

[javascript] view plaincopyprint?
  1. <script>  
  2.   
  3. alert("頂部腳本");  
  4.   
  5. </script>  
  6.   
  7. <html>  
  8.   
  9. <head>  
  10.   
  11. <script src="head.js"></script>  
  12.   
  13. <title></title>  
  14.   
  15. </head>  
  16.   
  17. <body>  
  18.   
  19. <script src="body.js"></script>  
  20.   
  21. </body>  
  22.   
  23. </html>  
  24.   
  25. <script>  
  26.   
  27. alert("底部腳本");  
  28.   
  29. </script>  


1.2  預編譯與執行順序的關係

Javascript中,function纔是Javascript的第一型。當我們寫下一段函數時,其實不過是建立了一個function類型的實體。

就像我們可以寫成這樣的形式一樣:

function Hello() {

           alert("Hello");

       }

       Hello();

       var Hello = function() {

           alert("Hello");

       }

       Hello();

其實都是一樣的。

但是當我們對其中的函數進行修改時,會發現很奇怪的問題。

   <scripttype="text/javascript">

       function Hello(){

            alert("Hello");

       }

       Hello();

       functionHello() {

            alert("Hello World");

       }

       Hello();

    </script>

 

我們會看到這樣的結果:連續輸出了兩次Hello World。而非我們想象中的HelloHello World

這是因爲Javascript並非完全的按順序解釋執行,而是在解釋之前會對Javascript進行一次預編譯,在預編譯的過程中,會把定義式的函數優先執行,也會把所有var變量創建,默認值爲undefined,以提高程序的執行效率。也就是說上面的一段代碼其實被JS引擎預編譯爲這樣的形式:

    <scripttype="text/javascript">

        var Hello = function() {

            alert("Hello");

        }

        Hello = function() {

            alert("Hello World");

        }

        Hello();

        Hello();

    </script>

 

 

我們可以通過上面的代碼很清晰地看到,其實函數也是數據,也是變量,我們也可以對函數進行賦值(重賦值)。當然,我們爲了防止這樣的情況,也可以這樣:

 

 <scripttype="text/javascript">

        functionHello() {

            alert("Hello");

        }

        Hello();

    </script>

    <scripttype="text/javascript">

        functionHello() {

            alert("Hello World");

        }

        Hello();

    </script>

這樣,程序被分成了兩段,JS引擎也就不會把他們放到一起了。

 

當JavaScript引擎解析腳本時,它會在預編譯期對所有聲明的變量和函數進行處理。

做如下處理:

1. 在執行前會進行類似“預編譯”的操作:首先會創建一個當前執行環境下的

活動對象,並將那些用var申明的變量設置爲活動對象的屬性,但是此時這些變量的賦值都是undefined,並將那些以function定義的函數也添加爲活動對象的屬性,而且它們的值正是函數的定義。

2. 在解釋執行階段,遇到變量需要解析時,會首先從當前執行環境的活動對象中查找,如果沒有找到而且該執行環境的擁有者有prototype屬性時則會從prototype鏈中查找,否則將會按照作用域鏈查找。遇到var a = ...這樣的語句時會給相應的變量進行賦值(注意:變量的賦值是在解釋執行階段完成的,如果在這之前使用變量,它的值會是undefined)
所以,就會出現當JavaScript解釋器執行下面腳本時不會報錯:

[javascript] view plaincopyprint?
  1. alert(a);                            // 返回值undefined  
  2.   
  3. var a=1;  
  4.   
  5. alert(a);                            // 返回值1  


由於變量聲明是在預編譯期被處理的,所以在執行期間對於所有代碼來說,都是可見的。但是,你也會看到,執行上面代碼,提示的值是undefined,而不是1。這是因爲,變量初始化過程發生在執行期,而不是預編譯期。在執行期,JavaScript解釋器是按着代碼先後順序進行解析的,如果在前面代碼行中沒有爲變量賦值,則JavaScript解釋器會使用默認值undefined。由於在第二行中爲變量a賦值了,所以在第三行代碼中會提示變量a的值爲1,而不是undefined。

同理,下面示例在函數聲明前調用函數也是合法的,並能夠被正確解析,所以返回值爲1

[javascript] view plaincopyprint?
  1. f();                                 // 調用函數,返回值1  
  2.   
  3. function f(){  
  4.   
  5.     alert(1);  
  6.   
  7. }  


但是,如果按下面方式定義函數,則JavaScript解釋器會提示語法錯誤。

[javascript] view plaincopyprint?
  1. f();              // 調用函數,返回語法錯誤(缺少對象)  
  2.   
  3. var f= function(){  
  4.   
  5.     alert(1);  
  6.   
  7. }  


這是因爲,上面示例中定義的函數僅作爲值賦值給變量f,所以在預編譯期,JavaScript解釋器只能夠爲聲明變量f進行處理,而對於變量f的值,只能等到執行期時按順序進行賦值,自然就會出現語法錯誤,提示找不到對象f。

再見一些例子:

 

[javascript] view plaincopyprint?
  1. <scripttype="text/javascript">  
  2.   
  3. /*在預編譯過程中func是window環境下的活動對象中的一個屬性,值是一個函數,覆蓋了undefined值*/  
  4.   
  5. alert(func);//function func(){alert("hello!")}  
  6.   
  7. varfunc = "this is a variable"  
  8.   
  9. functionfunc(){  
  10.   
  11. alert("hello!")  
  12.   
  13. }  
  14.   
  15. /*在執行過程中遇到了var重新賦值爲"thisis a variable"*/  
  16.   
  17. alert(func);  //this is a variable  
  18.   
  19. </script>  
  20.   
  21. //對於下面這個例子,經過我的驗證不正確  
  22.   
  23. <scripttype="text/javascript">  
  24. var name = "feng";  
  25. function func(){  
  26. /*首先,在func環境內先把name賦值爲undefined,然後在執行過程中先尋找func環境下的活動對象的name屬性,此時之前已經預編譯值爲undefined,所以輸出是undefined,而不是feng*/  
  27. alert(name);  //undefined  我驗證的結果是:“feng”  
  28. var name = "JSF";  
  29. alert(name);  //JSF  
  30. }  
  31. func();  
  32. alert(name); //feng  
  33. </script>  


雖然變量和函數聲明可以在文檔任意位置,但是良好的習慣應該是在所有JavaScript代碼之前聲明全局變量和函數,並對變量進行初始化賦值。在函數內部也是先聲明變量,然後再引用。

 

 

 

 

 

 

 

 

1.3  按塊執行JavaScript代碼

所謂代碼塊就是使用<script>標籤分隔的代碼段。例如,下面兩個<script>標籤分別代表兩個JavaScript代碼塊。

[javascript] view plaincopyprint?
  1. <script>  
  2.   
  3. // JavaScript代碼塊1  
  4.   
  5. var a =1;  
  6.   
  7. </script>  
  8.   
  9. <script>  
  10.   
  11. // JavaScript代碼塊2  
  12.   
  13. function f(){  
  14.   
  15.     alert(1);  
  16.   
  17. }  
  18.   
  19. </script>  


JavaScript解釋器在執行腳本時,是按塊來執行的。通俗地說,就是瀏覽器在解析HTML文檔流時,如果遇到一個<script>標籤,則JavaScript解釋器會等到這個代碼塊都加載完後,先對代碼塊進行預編譯,然後再執行。執行完畢後,瀏覽器會繼續解析下面的HTML文檔流,同時JavaScript解釋器也準備好處理下一個代碼塊。

由於JavaScript是按塊執行的,所以如果在一個JavaScript塊中調用後面塊中聲明的變量或函數就會提示語法錯誤。例如,當JavaScript解釋器執行下面代碼時就會提示語法錯誤,顯示變量a未定義,對象f找不到。

[javascript] view plaincopyprint?
  1. <script>  
  2.   
  3. //JavaScript代碼塊1  
  4.   
  5. alert(a);  
  6.   
  7. f();  
  8.   
  9. </script>  
  10.   
  11. <script>  
  12.   
  13. //JavaScript代碼塊2  
  14.   
  15. var a=1;  
  16.   
  17. functionf(){  
  18.   
  19.     alert(1);  
  20.   
  21. }  
  22.   
  23. </script>  


雖然說,JavaScript是按塊執行的,但是不同塊都屬於同一個全局作用域,也就是說,塊之間的變量和函數是可以共享的。

1.6.4  藉助事件機制改變JavaScript執行順序

由於JavaScript是按塊處理代碼,同時又遵循HTML文檔流的解析順序,所以在上面示例中會看到這樣的語法錯誤。但是當文檔流加載完畢,如果再次訪問就不會出現這樣的錯誤。例如,把訪問第2塊代碼中的變量和函數的代碼放在頁面初始化事件函數中,就不會出現語法錯誤了。

[javascript] view plaincopyprint?
  1. <script>  
  2.   
  3. //JavaScript代碼塊1  
  4.   
  5. window.onload= function(){        // 頁面初始化事件處理函數  
  6.   
  7.     alert(a);  
  8.   
  9.     f();  
  10.   
  11. }  
  12.   
  13. </script>  
  14.   
  15. <script>  
  16.   
  17. //JavaScript代碼塊2  
  18.   
  19. var a =1;  
  20.   
  21. functionf(){  
  22.   
  23.     alert(1);  
  24.   
  25. }  
  26.   
  27. </script>  


爲了安全起見,我們一般在頁面初始化完畢之後才允許JavaScript代碼執行,這樣可以避免網速對JavaScript執行的影響,同時也避開了HTML文檔流對於JavaScript執行的限制。

注意

如果在一個頁面中存在多個windows.onload事件處理函數,則只有最後一個纔是有效的,爲了解決這個問題,可以把所有腳本或調用函數都放在同一個onload事件處理函數中,例如:

[javascript] view plaincopyprint?
  1. window.onload = function(){  
  2.   
  3.     f1();  
  4.   
  5.     f2();  
  6.   
  7.     f3();  
  8.   
  9. }  


而且通過這種方式可以改變函數的執行順序,方法是:簡單地調整onload事件處理函數中調用函數的排列順序。

除了頁面初始化事件外,我們還可以通過各種交互事件來改變JavaScript代碼的執行順序,如鼠標事件、鍵盤事件及時鐘觸發器等方法,詳細講解請參閱第14章的內容。

1.6.5  JavaScript輸出腳本的執行順序

在JavaScript開發中,經常會使用document對象的write()方法輸出JavaScript腳本。那麼這些動態輸出的腳本是如何執行的呢?例如:

[javascript] view plaincopyprint?
  1. document.write('<scripttype="text/javascript">');  
  2.   
  3. document.write('f();');  
  4.   
  5. document.write('functionf(){ ');  
  6.   
  7. document.write('    alert(1);  ');  
  8.   
  9. document.write('}    ');  
  10.   
  11. document.write('<\/script>    ');  


運行上面代碼,我們會發現:document.write()方法先把輸出的腳本字符串寫入到腳本所在的文檔位置,瀏覽器在解析完document.write()所在文檔內容後,繼續解析document.write()輸出的內容,然後才按順序解析後面的HTML文檔。也就是說,JavaScript腳本輸出的代碼字符串會在輸出後馬上被執行。

請注意,使用document.write()方法輸出的JavaScript腳本字符串必須放在同時被輸出的<script>標籤中,否則JavaScript解釋器因爲不能夠識別這些合法的JavaScript代碼,而作爲普通的字符串顯示在頁面文檔中。例如,下面的代碼就會把JavaScript代碼顯示出來,而不是執行它。

[javascript] view plaincopyprint?
  1. document.write('f();');  
  2.   
  3. document.write('functionf(){ ');  
  4.   
  5. document.write('    alert(1);  ');  
  6.   
  7. document.write(');  ');  


但是,通過document.write()方法輸出腳本並執行也存在一定的風險,因爲不同JavaScript引擎對其執行順序不同,同時不同瀏覽器在解析時也會出現Bug。

Ø 問題一,找不到通過document.write()方法導入的外部JavaScript文件中聲明的變量或函數。例如,看下面示例代碼。


[javascript] view plaincopyprint?
  1. document.write('<scripttype="text/javascript" src="test.js">  
  2.   
  3. <\/script>');  
  4.   
  5. document.write('<scripttype="text/javascript">  ');  
  6.   
  7. document.write('alert(n);');  // IE提示找不到變量n  
  8.   
  9. document.write('<\/script>    ');  
  10.   
  11. alert(n+1);                          // 所有瀏覽器都會提示找不到變量n  


外部JavaScript文件(test.js)的代碼如下:

[javascript] view plaincopyprint?
  1. var n = 1;  


分別在不同瀏覽器中進行測試,會發現提示語法錯誤,找不到變量n。也就是說,如果在JavaScript代碼塊中訪問本代碼塊中使用document.write()方法輸出的腳本中導入的外部JavaScript文件所包含的變量,會顯示語法錯誤。同時,如果在IE瀏覽器中,不僅在腳本中,而且在輸出的腳本中也會提示找不到輸出的導入外部JavaScript文件的變量(表述有點長和繞,不懂的讀者可以嘗試運行上面代碼即可明白)。

Ø 問題二,不同JavaScript引擎對輸出的外部導入腳本的執行順序略有不同。例如,看下面示例代碼。

[javascript] view plaincopyprint?
  1. <scripttype="text/javascript">  
  2.   
  3. document.write('<scripttype="text/javascript" src="test1.js">  
  4.   
  5. <\/script>');  
  6.   
  7. document.write('<scripttype="text/javascript">  ');  
  8.   
  9. document.write('alert(2);')  
  10.   
  11. document.write('alert(n+2);');  
  12.   
  13. document.write('<\/script>');  
  14.   
  15. </script>  
  16.   
  17. <scripttype="text/javascript">  
  18.   
  19. alert(n+3);  
  20.   
  21. </script>  


外部JavaScript文件(test1.js)的代碼如下所示。

[javascript] view plaincopyprint?
  1. var n = 1;  
  2.   
  3. alert(n);  


在IE瀏覽器中的執行順序如圖1-6所示。


圖1-6  IE 7瀏覽器的執行順序和提示的語法錯誤

在符合DOM標準的瀏覽器中的執行順序與IE瀏覽器不同,且沒有語法錯誤,如圖1-7所示的是在Firefox 3.0瀏覽器中的執行順序。


圖1-7  Firefox 3瀏覽器的執行順序和提示的語法錯誤

解決不同瀏覽器存在的不同執行順序,以及可能存在Bug。我們可以把凡是使用輸出腳本導入的外部文件,都放在獨立的代碼塊中,這樣根據上面介紹的JavaScript代碼塊執行順序,就可以避免這個問題。例如,針對上面示例,可以這樣設計:

[javascript] view plaincopyprint?
  1. <scripttype="text/javascript">  
  2.   
  3. document.write('<scripttype="text/javascript"src="test1.js"><\/script>');  
  4.   
  5. </script>  
  6.   
  7. <scripttype="text/javascript">  
  8.   
  9. document.write('<scripttype="text/javascript">  ');  
  10.   
  11. document.write('alert(2);') ;           // 提示2  
  12.   
  13. document.write('alert(n+2);   ');     // 提示3  
  14.   
  15. document.write('<\/script>    ');  
  16.   
  17. alert(n+3);                                    // 提示4  
  18.   
  19. </script>  
  20.   
  21. <scripttype="text/javascript">  
  22.   
  23. alert(n+4);                                    // 提示5  
  24.   
  25. </script>  


這樣在不同瀏覽器中都能夠按順序執行上面代碼,且輸出順序都是1、2、3、4和5。存在問題的原因是:輸出導入的腳本與當前JavaScript代碼塊之間的矛盾。如果單獨輸出就不會發生衝突了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章