讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議167:準確分析JavaScript執行順序 >

建議167:準確分析JavaScript執行順序

JavaScript引擎的工作機制比較深奧,它屬於底層行為。JavaScript代碼執行順序就比較形象了,用戶可以直觀感受到這種執行順序,當然,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=\"head.js\"></script>

<title></title>

</head>

<body>

<script src=\"body.js\"></script>

</body>

</html>

<script>

alert(\"底部腳本\");

</script>


當JavaScript引擎解析腳本時,它會在預編譯期對所有聲明的變量和函數進行處理,因此,就會出現當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。

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

同時,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是按塊執行的,但是不同塊都屬於同一個全局作用域,也就是說,塊之間的變量和函數是可以共享的。

由於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執行的限制。

如果一個頁面中存在多個window.onload事件處理函數,那麼只有最後一個才是有效的,為了解決這個問題,可以把所有腳本或調用函數都放在同一個onload事件處理函數中。


window.onload=function{

f1;

f2;

f3;

}


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

除了頁面初始化事件外,還可以通過各種交互事件來改變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=\"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=\"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);


在IE中的執行順序如圖9.5所示。

圖 9.5 在IE中的執行順序和提示的語法錯誤

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

圖 9.6 在Firefox瀏覽器中的執行順序

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


<script type=\"text/javascript\">

document.write(\'<script type=\"text/javascript\"src=\"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代碼塊之間的矛盾。單獨輸出就不會發生衝突了。