JavaScript執(zhí)行順序詳細(xì)介紹
之前從JavaScript引擎的解析機(jī)制來探索JavaScript的工作原理,下面我們以更形象的示例來說明JavaScript代碼在頁面中的執(zhí)行順序。如果說,JavaScript引擎的工作機(jī)制比較深?yuàn)W是因?yàn)樗鼘儆诘讓有袨椋敲碕avaScript代碼執(zhí)行順序就比較形象了,因?yàn)槲覀兛梢灾庇^感覺到這種執(zhí)行順序,當(dāng)然JavaScript代碼的執(zhí)行順序是比較復(fù)雜的,所以在深入JavaScript語言之前也有必要對(duì)其進(jìn)行剖析。
1.1 按HTML文檔流順序執(zhí)行JavaScript代碼
首先,讀者應(yīng)該清楚,HTML文檔在瀏覽器中的解析過程是這樣的:瀏覽器是按著文檔流從上到下逐步解析頁面結(jié)構(gòu)和信息的。JavaScript代碼作為嵌入的腳本應(yīng)該也算做HTML文檔的組成部分,所以JavaScript代碼在裝載時(shí)的執(zhí)行順序也是根據(jù)腳本標(biāo)簽<script>的出現(xiàn)順序來確定的。例如,瀏覽下面文檔頁面,你會(huì)看到代碼是從上到下逐步被解析的。
<script>
alert("頂部腳本");
</script>
<html><head>
<script>
alert("頭部腳本");
</script>
<title></title>
</head>
<body>
<script>
alert("頁面腳本");
</script>
</body></html>
<script>
alert("底部腳本");
</script>
如果通過腳本標(biāo)簽<script>的src屬性導(dǎo)入外部JavaScript文件腳本,那么它也將按照其語句出現(xiàn)的順序來執(zhí)行,而且執(zhí)行過程是文檔裝載的一部分。不會(huì)因?yàn)槭峭獠縅avaScript文件而延期執(zhí)行。例如,把上面文檔中的頭部和主體區(qū)域的腳本移到外部JavaScript文件中,然后通過src屬性導(dǎo)入。繼續(xù)預(yù)覽頁面文檔,你會(huì)看到相同的執(zhí)行順序。
<script>
alert("頂部腳本");
</script>
<html>
<head>
<script src="http://www.dbjr.com.cn/head.js"></script>
<title></title>
</head>
<body>
<script src="http://www.dbjr.com.cn/body.js"></script>
</body>
</html>
<script>
alert("底部腳本");
</script>
1.2 預(yù)編譯與執(zhí)行順序的關(guān)系
在Javascript中,function才是Javascript的第一型。當(dāng)我們寫下一段函數(shù)時(shí),其實(shí)不過是建立了一個(gè)function類型的實(shí)體。
就像我們可以寫成這樣的形式一樣:
functionHello()
{
alert("Hello");
}
Hello();
varHello = function()
{
alert("Hello");
}
Hello();
其實(shí)都是一樣的。 但是當(dāng)我們對(duì)其中的函數(shù)進(jìn)行修改時(shí),會(huì)發(fā)現(xiàn)很奇怪的問題。
<scripttype="text/javascript">
functionHello() {
alert("Hello");
}
Hello();
functionHello() {
alert("Hello World");
}
Hello();
</script>
我們會(huì)看到這樣的結(jié)果:連續(xù)輸出了兩次Hello World。
而非我們想象中的Hello和Hello World。
這是因?yàn)镴avascript并非完全的按順序解釋執(zhí)行,而是在解釋之前會(huì)對(duì)Javascript進(jìn)行一次“預(yù)編譯”,在預(yù)編譯的過程中,會(huì)把定義式的函數(shù)優(yōu)先執(zhí)行,也會(huì)把所有var變量創(chuàng)建,默認(rèn)值為undefined,以提高程序的執(zhí)行效率。
也就是說上面的一段代碼其實(shí)被JS引擎預(yù)編譯為這樣的形式:
<scripttype="text/javascript">
varHello = function() {
alert("Hello");
}
Hello = function() {
alert("Hello World");
}
Hello();
Hello();
</script>
我們可以通過上面的代碼很清晰地看到,其實(shí)函數(shù)也是數(shù)據(jù),也是變量,我們也可以對(duì)“函數(shù)“進(jìn)行賦值(重賦值)。
當(dāng)然,我們?yōu)榱朔乐惯@樣的情況,也可以這樣:
<scripttype="text/javascript">
functionHello() {
alert("Hello");
}
Hello();
</script>
<scripttype="text/javascript">
functionHello() {
alert("Hello World");
}
Hello();
</script>
這樣,程序被分成了兩段,JS引擎也就不會(huì)把他們放到一起了。
當(dāng)JavaScript引擎解析腳本時(shí),它會(huì)在預(yù)編譯期對(duì)所有聲明的變量和函數(shù)進(jìn)行處理。
做如下處理:
1. 在執(zhí)行前會(huì)進(jìn)行類似“預(yù)編譯”的操作:首先會(huì)創(chuàng)建一個(gè)當(dāng)前執(zhí)行環(huán)境下的活動(dòng)對(duì)象,并將那些用var申明的變量設(shè)置為活動(dòng)對(duì)象的屬性,但是此時(shí)這些變量的賦值都是undefined,并將那些以function定義的函數(shù)也添加為活動(dòng)對(duì)象的屬性,而且它們的值正是函數(shù)的定義。
2. 在解釋執(zhí)行階段,遇到變量需要解析時(shí),會(huì)首先從當(dāng)前執(zhí)行環(huán)境的活動(dòng)對(duì)象中查找,如果沒有找到而且該執(zhí)行環(huán)境的擁有者有prototype屬性時(shí)則會(huì)從prototype鏈中查找,否則將會(huì)按照作用域鏈查找。遇到var a = ...這樣的語句時(shí)會(huì)給相應(yīng)的變量進(jìn)行賦值(注意:變量的賦值是在解釋執(zhí)行階段完成的,如果在這之前使用變量,它的值會(huì)是undefined) 所以,就會(huì)出現(xiàn)當(dāng)JavaScript解釋器執(zhí)行下面腳本時(shí)不會(huì)報(bào)錯(cuò):
alert(a); // 返回值undefined
var a =1;
alert(a); // 返回值1
由于變量聲明是在預(yù)編譯期被處理的,所以在執(zhí)行期間對(duì)于所有代碼來說,都是可見的。但是,你也會(huì)看到,執(zhí)行上面代碼,提示的值是undefined,而不是1。這是因?yàn)?,變量初始化過程發(fā)生在執(zhí)行期,而不是預(yù)編譯期。在執(zhí)行期,JavaScript解釋器是按著代碼先后順序進(jìn)行解析的,如果在前面代碼行中沒有為變量賦值,則JavaScript解釋器會(huì)使用默認(rèn)值undefined。由于在第二行中為變量a賦值了,所以在第三行代碼中會(huì)提示變量a的值為1,而不是undefined。
同理,下面示例在函數(shù)聲明前調(diào)用函數(shù)也是合法的,并能夠被正確解析,所以返回值為1。
f(); // 調(diào)用函數(shù),返回值1
function f(){
alert(1);
}
但是,如果按下面方式定義函數(shù),則JavaScript解釋器會(huì)提示語法錯(cuò)誤。
f(); // 調(diào)用函數(shù),返回語法錯(cuò)誤
var f = function(){
alert(1);
}
這是因?yàn)椋厦媸纠卸x的函數(shù)僅作為值賦值給變量f,所以在預(yù)編譯期,JavaScript解釋器只能夠?yàn)槁暶髯兞縡進(jìn)行處理,而對(duì)于變量f的值,只能等到執(zhí)行期時(shí)按順序進(jìn)行賦值,自然就會(huì)出現(xiàn)語法錯(cuò)誤,提示找不到對(duì)象f。
再見一些例子:
<script type="text/javascript">
/*在預(yù)編譯過程中func是window環(huán)境下的活動(dòng)對(duì)象中的一個(gè)屬性,值是一個(gè)函數(shù),覆蓋了undefined值*/
alert(func); //function func(){alert("hello!")}
var func = "this is a variable"
function func(){
alert("hello!")
}
/*在執(zhí)行過程中遇到了var重新賦值為"this is a variable"*/
alert(func); //this is a variable
</script>
<script type="text/javascript">
var name = "feng"; function func()
{
/*首先,在func環(huán)境內(nèi)先把name賦值為undefined,然后在執(zhí)行過程中先尋找func環(huán)境下的活動(dòng)對(duì)象的name屬性,此時(shí)之前已經(jīng)預(yù)編譯值為undefined,所以輸出是undefined,而不是feng*/
alert(name); //undefined var name = "JSF";
alert(name); //JSF
}
func();
alert(name);
//feng
</script>
雖然變量和函數(shù)聲明可以在文檔任意位置,但是良好的習(xí)慣應(yīng)該是在所有JavaScript代碼之前聲明全局變量和函數(shù),并對(duì)變量進(jìn)行初始化賦值。在函數(shù)內(nèi)部也是先聲明變量,然后再引用。
1.3 按塊執(zhí)行JavaScript代碼
所謂代碼塊就是使用<script>標(biāo)簽分隔的代碼段。例如,下面兩個(gè)<script>標(biāo)簽分別代表兩個(gè)JavaScript代碼塊。
<script>
// JavaScript代碼塊1
var a =1;
</script>
<script>
// JavaScript代碼塊2
function f(){
alert(1);
}
</script>
JavaScript解釋器在執(zhí)行腳本時(shí),是按塊來執(zhí)行的。通俗地說,就是瀏覽器在解析HTML文檔流時(shí),如果遇到一個(gè)<script>標(biāo)簽,則JavaScript解釋器會(huì)等到這個(gè)代碼塊都加載完后,先對(duì)代碼塊進(jìn)行預(yù)編譯,然后再執(zhí)行。執(zhí)行完畢后,瀏覽器會(huì)繼續(xù)解析下面的HTML文檔流,同時(shí)JavaScript解釋器也準(zhǔn)備好處理下一個(gè)代碼塊。
由于JavaScript是按塊執(zhí)行的,所以如果在一個(gè)JavaScript塊中調(diào)用后面塊中聲明的變量或函數(shù)就會(huì)提示語法錯(cuò)誤。例如,當(dāng)JavaScript解釋器執(zhí)行下面代碼時(shí)就會(huì)提示語法錯(cuò)誤,顯示變量a未定義,對(duì)象f找不到。
<script>
// JavaScript代碼塊1
alert(a);
f();
</script>
<script>
// JavaScript代碼塊2
var a =1;
function f(){
alert(1);
}
</script>
雖然說,JavaScript是按塊執(zhí)行的,但是不同塊都屬于同一個(gè)全局作用域,也就是說,塊之間的變量和函數(shù)是可以共享的。
1.4 借助事件機(jī)制改變JavaScript執(zhí)行順序
由于JavaScript是按塊處理代碼,同時(shí)又遵循HTML文檔流的解析順序,所以在上面示例中會(huì)看到這樣的語法錯(cuò)誤。但是當(dāng)文檔流加載完畢,如果再次訪問就不會(huì)出現(xiàn)這樣的錯(cuò)誤。例如,把訪問第2塊代碼中的變量和函數(shù)的代碼放在頁面初始化事件函數(shù)中,就不會(huì)出現(xiàn)語法錯(cuò)誤了。
<script>
// JavaScript代碼塊1
window.onload = function(){ // 頁面初始化事件處理函數(shù)
alert(a);
f();
}
</script>
<script>
// JavaScript代碼塊2
var a =1;
function f(){
alert(1);
}
</script>
為了安全起見,我們一般在頁面初始化完畢之后才允許JavaScript代碼執(zhí)行,這樣可以避免網(wǎng)速對(duì)JavaScript執(zhí)行的影響,同時(shí)也避開了HTML文檔流對(duì)于JavaScript執(zhí)行的限制。
注意
如果在一個(gè)頁面中存在多個(gè)windows.onload事件處理函數(shù),則只有最后一個(gè)才是有效的,為了解決這個(gè)問題,可以把所有腳本或調(diào)用函數(shù)都放在同一個(gè)onload事件處理函數(shù)中,例如:
window.onload = function(){
f1();
f2();
f3();
}
而且通過這種方式可以改變函數(shù)的執(zhí)行順序,方法是:簡(jiǎn)單地調(diào)整onload事件處理函數(shù)中調(diào)用函數(shù)的排列順序。
除了頁面初始化事件外,我們還可以通過各種交互事件來改變JavaScript代碼的執(zhí)行順序,如鼠標(biāo)事件、鍵盤事件及時(shí)鐘觸發(fā)器等方法,詳細(xì)講解請(qǐng)參閱第14章的內(nèi)容。
1.5 JavaScript輸出腳本的執(zhí)行順序
在JavaScript開發(fā)中,經(jīng)常會(huì)使用document對(duì)象的write()方法輸出JavaScript腳本。那么這些動(dòng)態(tài)輸出的腳本是如何執(zhí)行的呢?例如:
document.write('<script type="text/javascript">');
document.write('f();');
document.write('function f(){');
document.write('alert(1);');
document.write('}');
document.write('</script>');
運(yùn)行上面代碼,我們會(huì)發(fā)現(xiàn):document.write()方法先把輸出的腳本字符串寫入到腳本所在的文檔位置,瀏覽器在解析完document.write()所在文檔內(nèi)容后,繼續(xù)解析document.write()輸出的內(nèi)容,然后才按順序解析后面的HTML文檔。也就是說,JavaScript腳本輸出的代碼字符串會(huì)在輸出后馬上被執(zhí)行。
請(qǐng)注意,使用document.write()方法輸出的JavaScript腳本字符串必須放在同時(shí)被輸出的<script>標(biāo)簽中,否則JavaScript解釋器因?yàn)椴荒軌蜃R(shí)別這些合法的JavaScript代碼,而作為普通的字符串顯示在頁面文檔中。例如,下面的代碼就會(huì)把JavaScript代碼顯示出來,而不是執(zhí)行它。
document.write('f();');
document.write('function f(){');
document.write('alert(1);');
document.write(');');
但是,通過document.write()方法輸出腳本并執(zhí)行也存在一定的風(fēng)險(xiǎn),因?yàn)椴煌琂avaScript引擎對(duì)其執(zhí)行順序不同,同時(shí)不同瀏覽器在解析時(shí)也會(huì)出現(xiàn)Bug。
Ø 問題一,找不到通過document.write()方法導(dǎo)入的外部JavaScript文件中聲明的變量或函數(shù)。例如,看下面示例代碼。
document.write('<script type="text/javascript" src="http://www.dbjr.com.cn/test.js">
</script>');
document.write('<script type="text/javascript">');
document.write('alert(n);'); // IE提示找不到變量n
document.write('</script>');
alert(n+1); // 所有瀏覽器都會(huì)提示找不到變量n
外部JavaScript文件(test.js)的代碼如下:
var n = 1;
分別在不同瀏覽器中進(jìn)行測(cè)試,會(huì)發(fā)現(xiàn)提示語法錯(cuò)誤,找不到變量n。也就是說,如果在JavaScript代碼塊中訪問本代碼塊中使用document.write()方法輸出的腳本中導(dǎo)入的外部JavaScript文件所包含的變量,會(huì)顯示語法錯(cuò)誤。同時(shí),如果在IE瀏覽器中,不僅在腳本中,而且在輸出的腳本中也會(huì)提示找不到輸出的導(dǎo)入外部JavaScript文件的變量(表述有點(diǎn)長(zhǎng)和繞,不懂的讀者可以嘗試運(yùn)行上面代碼即可明白)。
Ø 問題二,不同JavaScript引擎對(duì)輸出的外部導(dǎo)入腳本的執(zhí)行順序略有不同。例如,看下面示例代碼。
<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);
在IE瀏覽器中的執(zhí)行順序如圖1-6所示。
圖1-6 IE 7瀏覽器的執(zhí)行順序和提示的語法錯(cuò)誤
在符合DOM標(biāo)準(zhǔn)的瀏覽器中的執(zhí)行順序與IE瀏覽器不同,且沒有語法錯(cuò)誤,如圖1-7所示的是在Firefox 3.0瀏覽器中的執(zhí)行順序。
圖1-7 Firefox 3瀏覽器的執(zhí)行順序和提示的語法錯(cuò)誤
解決不同瀏覽器存在的不同執(zhí)行順序,以及可能存在Bug。我們可以把凡是使用輸出腳本導(dǎo)入的外部文件,都放在獨(dú)立的代碼塊中,這樣根據(jù)上面介紹的JavaScript代碼塊執(zhí)行順序,就可以避免這個(gè)問題。例如,針對(duì)上面示例,可以這樣設(shè)計(jì):
<script type="text/javascript">
document.write('<script type="text/javascript" src="http://www.dbjr.com.cn/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>
這樣在不同瀏覽器中都能夠按順序執(zhí)行上面代碼,且輸出順序都是1、2、3、4和5。存在問題的原因是:輸出導(dǎo)入的腳本與當(dāng)前JavaScript代碼塊之間的矛盾。如果單獨(dú)輸出就不會(huì)發(fā)生沖突了。
相關(guān)文章
簡(jiǎn)單掌握J(rèn)avaScript中const聲明常量與變量的用法
const和let一樣,也是ES6版本中引入的新關(guān)鍵字,下面我們就通過例子來簡(jiǎn)單掌握J(rèn)avaScript中const關(guān)鍵詞聲明常量與變量的用法2016-05-05JavaScript學(xué)習(xí)筆記(十七)js 優(yōu)化
在JavaScript中,我們可以使用for(;;),while(),for(in)三種循環(huán),事實(shí)上,這三種循環(huán)中for(in)的效率極差,因?yàn)樗枰樵兩⒘墟I,只要可以就應(yīng)該盡量少用。2010-02-02關(guān)于JavaScript 原型鏈的一點(diǎn)個(gè)人理解
本文給大家分享的是個(gè)人關(guān)于JavaScript原型鏈相關(guān)知識(shí)的一些理解,這里推薦給大家,希望大家能夠喜歡2016-07-07服務(wù)端 VBScript 與 JScript 幾個(gè)相同特性的寫法 By shawl.qiu
服務(wù)端 VBScript 與 JScript 幾個(gè)相同特性的寫法 By shawl.qiu...2007-03-03簡(jiǎn)介JavaScript中Math.LOG10E屬性的使用
這篇文章主要介紹了JavaScript中Math.LOG10E屬性的使用,是JS入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-06-06JavaScript操作HTML DOM節(jié)點(diǎn)的基礎(chǔ)教程
這篇文章主要介紹了JavaScript操作HTML DOM節(jié)點(diǎn)的基礎(chǔ)入門教程,包括對(duì)節(jié)點(diǎn)的創(chuàng)建修改刪除等操作,還特別提到了其中appendChild()與insertBefore()插入節(jié)點(diǎn)時(shí)需注意的問題,需要的朋友可以參考下2016-03-03