1. 程式人生 > >理解JavaScript之JavaScript執行順序

理解JavaScript之JavaScript執行順序

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

1.1 按HTML文件流順序執行JavaScript程式碼

首先,讀者應該清楚,HTML文件在瀏覽器中的解析過程是這樣的:瀏覽器是按著文件流從上到下逐步解析頁面結構和資訊的。JavaScript程式碼作為嵌入的指令碼應該也算做HTML文件的組成部分,所以JavaScript程式碼在裝載時的執行順序也是根據指令碼標籤<script>

的出現順序來確定的。例如,瀏覽下面文件頁面,你會看到程式碼是從上到下逐步被解析的。
程式碼如下:

<script>
    alert("頂部指令碼");
</script>
<html>
<head>
<script>
    alert("頭部指令碼");
</script>
<title></title>
</head>
<body>
    <script>
        alert("頁面指令碼");
    </script>
</body
>
</html> <script> alert("底部指令碼"); </script>

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

<script>
    alert("頂部指令碼");
</script
>
<html> <head> <script src="http://www.jb51.net/head.js"></script> <title></title> </head> <body> <script src="http://www.jb51.net/body.js"></script> </body> </html> <script> alert("底部指令碼"); </script>

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

在Javascript中,function才是Javascript的第一型。當我們寫下一段函式時,其實不過是建立了一個function型別的實體。
就像我們可以寫成這樣的形式一樣:
程式碼如下:

        function Hello()
        {
            alert("Hello");
        }
        Hello();
        varHello = function() {
            alert("Hello");
        }
        Hello();

其實都是一樣的。 但是當我們對其中的函式進行修改時,會發現很奇怪的問題。
程式碼如下:

<script type="text/javascript">
    function Hello()
    {
        alert("Hello");
    }
    Hello();
    function Hello()
    {
        alert("Hello World");
    }
    Hello();
</script>

我們會看到這樣的結果:連續輸出了兩次Hello World。
而非我們想象中的Hello和Hello World。
這是因為Javascript並非完全的按順序解釋執行,而是在解釋之前會對Javascript進行一次“預編譯”,在預編譯的過程中,會把定義式的函式優先執行,也會把所有var變數建立,預設值為undefined,以提高程式的執行效率。
也就是說上面的一段程式碼其實被JS引擎預編譯為這樣的形式:
程式碼如下:

<script type="text/javascript">
    varHello = function() {
        alert("Hello");
    }
    Hello = function() {
        alert("Hello World");
    }
    Hello();
    Hello();
</script>

我們可以通過上面的程式碼很清晰地看到,其實函式也是資料,也是變數,我們也可以對“函式“進行賦值(重賦值)。
當然,我們為了防止這樣的情況,也可以這樣:
程式碼如下:

<script type="text/javascript">
    function Hello()
    {
        alert("Hello");
    }
    Hello();
</script>
<script type="text/javascript">
    function Hello()
    {
        alert("Hello World");
    }
    Hello();
</script>

這樣,程式被分成了兩段,JS引擎也就不會把他們放到一起了。
當JavaScript引擎解析指令碼時,它會在預編譯期對所有宣告的變數和函式進行處理。
做如下處理:

  1. 在執行前會進行類似“預編譯”的操作:首先會建立一個當前執行環境下的活動物件,並將那些用var申明的變數設定為活動物件的屬性,但是此時這些變數的賦值都是undefined,並將那些以function定義的函式也新增為活動物件的屬性,而且它們的值正是函式的定義。

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

alert(a);                            // 返回值undefined
var a =1;
alert(a);                            // 返回值1

由於變數宣告是在預編譯期被處理的,所以在執行期間對於所有程式碼來說,都是可見的。但是,你也會看到,執行上面程式碼,提示的值是undefined,而不是1。這是因為,變數初始化過程發生在執行期,而不是預編譯期。在執行期,JavaScript直譯器是按著程式碼先後順序進行解析的,如果在前面程式碼行中沒有為變數賦值,則JavaScript直譯器會使用預設值undefined。由於在第二行中為變數a賦值了,所以在第三行程式碼中會提示變數a的值為1,而不是undefined。
同理,下面示例在函式宣告前呼叫函式也是合法的,並能夠被正確解析,所以返回值為1。
程式碼如下:

f();                                 // 呼叫函式,返回值1
function f(){
    alert(1);
}

但是,如果按下面方式定義函式,則JavaScript直譯器會提示語法錯誤。
程式碼如下:

f();                                 // 呼叫函式,返回語法錯誤
var f = function(){
    alert(1);
}

這是因為,上面示例中定義的函式僅作為值賦值給變數f,所以在預編譯期,JavaScript直譯器只能夠為宣告變數f進行處理,而對於變數f的值,只能等到執行期時按順序進行賦值,自然就會出現語法錯誤,提示找不到物件f。
再見一些例子:
程式碼如下:

<script type="text/javascript">
/*在預編譯過程中func是window環境下的活動物件中的一個屬性,值是一個函式,覆蓋了undefined值*/
alert(func); //function func(){alert("hello!")}
var func = "this is a variable"
function func(){
alert("hello!")
}
/*在執行過程中遇到了var重新賦值為"this is a variable"*/
alert(func);  //this is a variable
</script>

程式碼如下:

<script type="text/javascript"> 
var name = "feng"; function func()
{ 
/*首先,在func環境內先把name賦值為undefined,然後在執行過程中先尋找func環境下的活動物件的name屬性,此時之前已經預編譯值為undefined,所以輸出是undefined,而不是feng*/ 
alert(name);  //undefined var name = "JSF"; 
alert(name);  //JSF 
}
func(); 
alert(name); 
//feng
</script>

雖然變數和函式宣告可以在文件任意位置,但是良好的習慣應該是在所有JavaScript程式碼之前宣告全域性變數和函式,並對變數進行初始化賦值。在函式內部也是先宣告變數,然後再引用。

1.3 按塊執行JavaScript程式碼

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

<script>
// JavaScript程式碼塊1
var a =1;
</script>
<script>
// JavaScript程式碼塊2
function f(){
    alert(1);
}
</script>

JavaScript直譯器在執行指令碼時,是按塊來執行的。通俗地說,就是瀏覽器在解析HTML文件流時,如果遇到一個<script>標籤,則JavaScript直譯器會等到這個程式碼塊都載入完後,先對程式碼塊進行預編譯,然後再執行。執行完畢後,瀏覽器會繼續解析下面的HTML文件流,同時JavaScript直譯器也準備好處理下一個程式碼塊。
由於JavaScript是按塊執行的,所以如果在一個JavaScript塊中呼叫後面塊中宣告的變數或函式就會提示語法錯誤。例如,當JavaScript直譯器執行下面程式碼時就會提示語法錯誤,顯示變數a未定義,物件f找不到。
程式碼如下:

<script>
// JavaScript程式碼塊1
alert(a);
f();
</script>
<script>
// JavaScript程式碼塊2
var a =1;
function f(){
    alert(1);
}
</script>

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

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

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

<script>
// JavaScript程式碼塊1
window.onload = function(){        // 頁面初始化事件處理函式
    alert(a);
    f();
}
</script>
<script>
// JavaScript程式碼塊2
var a =1;
function f(){
    alert(1);
}
</script>

為了安全起見,我們一般在頁面初始化完畢之後才允許JavaScript程式碼執行,這樣可以避免網速對JavaScript執行的影響,同時也避開了HTML文件流對於JavaScript執行的限制。
注意
如果在一個頁面中存在多個windows.onload事件處理函式,則只有最後一個才是有效的,為了解決這個問題,可以把所有指令碼或呼叫函式都放在同一個onload事件處理函式中,例如:
程式碼如下:

window.onload = function(){
    f1();
    f2();
    f3();
}

而且通過這種方式可以改變函式的執行順序,方法是:簡單地調整onload事件處理函式中呼叫函式的排列順序。
除了頁面初始化事件外,我們還可以通過各種互動事件來改變JavaScript程式碼的執行順序,如滑鼠事件、鍵盤事件及時鐘觸發器等方法,詳細講解請參閱第14章的內容。

1.5 JavaScript輸出指令碼的執行順序

在JavaScript開發中,經常會使用document物件的write()方法輸出JavaScript指令碼。那麼這些動態輸出的指令碼是如何執行的呢?例如:
程式碼如下:

document.write('<script type="text/javascript">');
document.write('f();');
document.write('function f(){');
document.write('alert(1);');
document.write('}');
document.write('</script>');

執行上面程式碼,我們會發現:document.write()方法先把輸出的指令碼字串寫入到指令碼所在的文件位置,瀏覽器在解析完document.write()所在文件內容後,繼續解析document.write()輸出的內容,然後才按順序解析後面的HTML文件。也就是說,JavaScript指令碼輸出的程式碼字串會在輸出後馬上被執行。
請注意,使用document.write()方法輸出的JavaScript指令碼字串必須放在同時被輸出的<script>標籤中,否則JavaScript直譯器因為不能夠識別這些合法的JavaScript程式碼,而作為普通的字串顯示在頁面文件中。例如,下面的程式碼就會把JavaScript程式碼顯示出來,而不是執行它。
程式碼如下:

document.write('f();');
document.write('function f(){');
document.write('alert(1);');
document.write(');

但是,通過document.write()方法輸出指令碼並執行也存在一定的風險,因為不同JavaScript引擎對其執行順序不同,同時不同瀏覽器在解析時也會出現Bug。
Ø 問題一,找不到通過document.write()方法匯入的外部JavaScript檔案中宣告的變數或函式。例如,看下面示例程式碼。
程式碼如下:

document.write('<script type="text/javascript" src="http://www.jb51.net/test.js"></script>');
document.write('<script type="text/javascript">');
document.write('alert(n);');  // IE提示找不到變數n
document.write('</script>');
alert(n+1);                          // 所有瀏覽器都會提示找不到變數n

外部JavaScript檔案(test.js)的程式碼如下:
程式碼如下:

var n = 1;

分別在不同瀏覽器中進行測試,會發現提示語法錯誤,找不到變數n。也就是說,如果在JavaScript程式碼塊中訪問本程式碼塊中使用document.write()方法輸出的指令碼中匯入的外部JavaScript檔案所包含的變數,會顯示語法錯誤。同時,如果在IE瀏覽器中,不僅在指令碼中,而且在輸出的指令碼中也會提示找不到輸出的匯入外部JavaScript檔案的變數(表述有點長和繞,不懂的讀者可以嘗試執行上面程式碼即可明白)。
Ø 問題二,不同JavaScript引擎對輸出的外部匯入指令碼的執行順序略有不同。例如,看下面示例程式碼。
程式碼如下:

<script type="text/javascript">
document.write('<script type="text/javascript" src="http://shaozhuqing.com/test1.js">
</script>');
document.write('<script type="text/javascript">');
document.write('alert(2);')
document.write('alert(n+2);');
document.write('</script>');
</script>
<script type="text/javascript">
alert(n+3);
</script>

外部JavaScript檔案(test1.js)的程式碼如下所示。
程式碼如下:

var n = 1;
alert(n);

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

<script type="text/javascript">
document.write('<script type="text/javascript" src="http://www.jb51.net/test1.js"></script>');
</script>
<script type="text/javascript">
document.write('<script type="text/javascript">');
document.write('alert(2);') ; // 提示2
document.write('alert(n+2);'); // 提示3
document.write('</script>');
alert(n+3); // 提示4
</script>
<script type="text/javascript">
alert(n+4); // 提示5
</script>

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