大家都知道,JavaScript 代碼是需要在 JavaScript 引擎中運行的。我們在說到 JavaScript 運行的時候,常常會提到執行環境、詞法環境、作用域、執行上下文、閉包等內容。這些概念看起來都差不多,卻好像又不大容易區分清楚,它們分別都在描述什么呢?
這些詞語都是與 JavaScript 引擎執行代碼的過程有關,為了搞清楚這些概念之間的區別,我們可以回顧下 JavaScript 代碼運行過程中的各個階段。
JavaScript 是弱類型語言,在運行時才能確定變量類型。JavaScript 引擎在執行 JavaScript 代碼時,也會從上到下進行詞法分析、語法分析、語義分析等處理,并在代碼解析完成后生成 AST(抽象語法樹),最終根據 AST 生成 CPU 可以執行的機器碼并執行。
這個過程,我們稱之為語法分析階段。除了語法分析階段,JavaScript 引擎在執行代碼時還會進行其他的處理。以 V8 引擎為例,在 V8 引擎中 JavaScript 代碼的運行過程主要分成三個階段。
其中,語法分析階段屬于編譯器通用內容,就不再贅述。前面提到的執行環境、詞法環境、作用域、執行上下文等內容都是在編譯和執行階段中產生的概念。
執行上下文的創建離不開 JavaScript 的運行環境,JavaScript 運行環境包括全局環境、函數環境和eval,其中全局環境和函數環境的創建過程如下:
在不同的運行環境中,變量和函數可訪問的其他數據范圍不同,環境的行為(比如創建和銷毀)也有所區別。而每進入一個不同的運行環境時,JavaScript 都會創建一個新的執行上下文,該過程包括:
由于建立作用域鏈過程中會涉及變量對象的概念,因此我們先來看看變量對象的創建,再看建立作用域鏈和確定 this 的指向。
變量對象(VO)
每個執行上下文都會有一個關聯的變量對象,該對象上會保存這個上下文中定義的所有變量和函數。
在瀏覽器中,全局環境的變量對象是window對象,因此所有的全局變量和函數都是作為window對象的屬性和方法創建的。相應的,在 Node 中全局環境的變量對象則是global對象。
創建VO的過程
創建變量對象將會創建arguments對象(僅函數環境下),同時會檢查當前上下文的函數聲明和變量聲明。
變量聲明和函數聲明的處理過程,便是我們常說的變量提升和函數提升,其中函數聲明提升會優先于變量聲明提升。因為變量提升容易帶來變量在預期外被覆蓋掉的問題,同時還可能導致本應該被銷毀的變量沒有被銷毀等情況。因此 ES6 中引入了let和const關鍵字,從而使 JavaScript 也擁有了塊級作用域。
作用域
在各類編程語言中,作用域分為靜態作用域和動態作用域。JavaScript 采用的是詞法作用域(Lexical Scoping),也就是靜態作用域。詞法作用域中的變量,在編譯過程中會產生一個確定的作用域。
詞法作用域中的變量,在編譯過程中會產生一個確定的作用域,這個作用域即當前的執行上下文,在 ES5 后我們使用詞法環境(Lexical Environment)替代作用域來描述該執行上下文。因此,詞法環境可理解為我們常說的作用域,同樣也指當前的執行上下文(注意,是當前的執行上下文)。
在 JavaScript 中,詞法環境又分為詞法環境(Lexical Environment)和變量環境(Variable Environment)兩種,其中:
也就是說,創建變量過程中會進行函數提升和變量提升,JavaScript 會通過詞法環境來記錄函數和變量聲明。通過使用兩個詞法環境(而不是一個)分別記錄不同的變量聲明內容,JavaScript 實現了支持塊級作用域的同時,不影響原有的變量聲明和函數聲明。
這就是創建變量的過程,它屬于執行上下文創建中的一環。創建變量的過程會產生作用域,作用域也被稱為詞法環境。
作用域鏈,就是將各個作用域通過某種方式連接在一起。作用域就是詞法環境,而詞法環境由兩個成員組成。
通過外部詞法環境的引用,作用域可以層層拓展,建立起從里到外延伸的一條作用域鏈。當某個變量無法在自身詞法環境記錄中找到時,可以根據外部詞法環境引用向外層進行尋找,直到最外層的詞法環境中外部詞法環境引用為null,這便是作用域鏈的變量查詢。
JavaScript 代碼運行過程分為定義期和執行期,前面提到的編譯階段則屬于定義期,代碼示例如下:
function foo() { // 定義全局函數foo
console.dir(bar);
var a=1;
function bar() { // 在foo函數內部定義函數bar
a=2;
}
}
console.dir(foo);
foo();
前面我們說到,JavaScript 使用的是靜態作用域,因此函數的作用域在定義期已經決定了。在上面的例子中,全局函數foo創建了一個foo的[[scope]]屬性,包含了全局[[scope]]:
foo[[scope]]=[globalContext];
而當我們執行foo()時,也會分別進入foo函數的定義期和執行期。
在foo函數的定義期時,函數bar的[[scope]]將會包含全局[[scope]]和foo的[[scope]]:
bar[[scope]]=[fooContext, globalContext];
運行上述代碼,我們可以在控制臺看到符合預期的輸出:
可以看到:
也就是說,JavaScript 會通過外部詞法環境引用來創建變量對象的一個作用域鏈,從而保證對執行環境有權訪問的變量和函數的有序訪問。除了創建作用域鏈之外,在這個過程中還會對創建的變量對象做一些處理。
在編譯階段會進行變量對象(VO)的創建,該過程會進行函數聲明和變量聲明,這時候變量的值被初始化為 undefined。在代碼進入執行階段之后,JavaScript 會對變量進行賦值,此時變量對象會轉為活動對象(Active Object,簡稱 AO),轉換后的活動對象才可被訪問,這就是 VO -> AO 的過程,示例如下:
function foo(a) {
var b=2;
function c() {}
var d=function() {};
}
foo(1);
在執行foo(1)時,首先進入定義期,此時:
AO={
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function() c() {}
d:undefined
}
前面我們也有提到,進入執行期之后,會執行賦值語句進行賦值,此時變量b和d會被賦值為 2 和函數表達式:
AO={
arguments: {
0: 1,
length: 1
},
a: 1,
b: 2,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
這就是 VO -> AO 過程。
實際上在執行的時候,除了 VO 被激活,活動對象還會添加函數執行時傳入的參數和arguments這個特殊對象,因此 AO 和 VO 的關系可以用以下關系來表達:
AO=VO + function parameters + arguments
現在,我們知道作用域鏈是在進入代碼的執行階段時,通過外部詞法環境引用來創建的。總結如下:
通過作用域鏈,我們可以在函數內部可以直接讀取外部以及全局變量,但外部環境是無法訪問內部函數里的變量。示例如下:
function foo() {
var a=1;
}
foo();
console.log(a); // undefined
我們在全局環境下無法訪問函數foo中的變量a,這是因為全局函數的作用域鏈里,不含有函數foo內的作用域。
如果我們想要訪問內部函數的變量,可以通過函數foo中的函數bar返回變量a,并將函數bar返回,這樣我們在全局環境中也可以通過調用函數foo返回的函數bar,來訪問變量a:
function foo() {
var a=1;
function bar() {
return a;
}
return bar;
}
var b=foo();
console.log(b()); // 1
當函數執行結束之后,執行期上下文將被銷毀,其中包括作用域鏈和激活對象。
在上面的實例中;當b()執行時,foo函數上下文包括作用域都已經被銷毀了,但是foo作用域下的a依然可以被訪問到;這是因為bar函數引用了foo函數變量對象中的值,此時即使創建bar函數的foo函數執行上下文被銷毀了,但它的變量對象依然會保留在 JavaScript 內存中,bar函數依然可以通過bar函數的作用域鏈找到它,并進行訪問。這就是閉包;
閉包使得我們可以從外部讀取局部變量,常見的用途包括:
注意,在使用閉包的時候,需要及時清理不再使用到的變量,否則可能導致內存泄漏問題。
在 JavaScript 中,this指向執行當前代碼對象的所有者,可簡單理解為this指向最后調用當前代碼的那個對象。
根據 JavaScript 中函數的調用方式不同,this的指向分為以下情況。
可以看到,this在不同的情況下會有不同的指向,在 ES6 箭頭函數還沒出現之前,為了能正確獲取某個運行環境下this對象,我們常常會使用以下代碼:
var that=this;
var self=this;
這樣的代碼將變量分配給this,便于使用。但是降低了代碼可讀性,不推薦使用,通過正確使用箭頭函數,我們可以更好地管理作用域。
今天我們了解了 JavaScript 代碼的運行過程,該過程分為語法分析階段、編譯階段、執行階段三個階段。
在編譯階段,JavaScript會進行執行上下文的創建,在執行階段,變量對象(VO)會被激活為活動對象(AO),變量會進行賦值,此時活動對象才可被訪問。在執行結束之后,作用域鏈和活動對象均被銷毀,使用閉包可使活動對象依然被保留在內存中。這就是 JavaScript 代碼的運行過程。
這里是云端源想IT,幫你輕松學IT”
嗨~ 今天的你過得還好嗎?
我們總是先揚起塵土
然后抱怨自己看不見
- 2024.04.17 -
JavaScript是一種輕量級的編程語言,通常用于網頁開發,以增強用戶界面的交互性和動態性。然而在HTML中,有多種方法可以嵌入和使用JavaScript代碼。
本文就帶大家深入了解如何在HTML中使用JavaScript。
要在HTML中使用JavaScript,我們需要使用<script>標簽。這個標簽可以放在<head>或<body>部分,但通常我們會將其放在<body>部分的底部,以確保在執行JavaScript代碼時,HTML文檔已經完全加載。
使用 <script> 標簽有兩種方式:直接在頁面中嵌入 JavaScript 代碼和包含外部 JavaScript 文件。
包含在 <script> 標簽內的 JavaScript 代碼在瀏覽器總按照從上至下的順序依次解釋。
所有 <script> 標簽都會按照他們在 HTML 中出現的先后順序依次被解析。
HTML 為 <script> 定義了幾個屬性:
1)async:可選。表示應該立即下載腳本,但不妨礙頁面中其他操作。該功能只對外部 JavaScript 文件有效。
如果給一個外部引入的js文件設置了這個屬性,那頁面在解析代碼的時候遇到這個<script>的時候,一邊下載該腳本文件,一邊異步加載頁面其他內容。
2)defer:可選。表示腳本可以延遲到整個頁面完全被解析和顯示之后再執行。該屬性只對外部 JavaScript 文件有效。
3)src:可選。表示包含要執行代碼的外部文件。
4)type:可選。表示編寫代碼使用的腳本語言的內容類型,目前在客戶端,type 屬性值一般使用 text/javascript。不過這個屬性并不是必需的,如果沒有指定這個屬性,則其默認值仍為text/javascript。
1.1 直接在頁面中嵌入JavaScript代碼
內部JavaScript是將JavaScript代碼放在HTML文檔的<script>標簽中。這樣可以將JavaScript代碼與HTML代碼分離,使結構更清晰,易于維護。
在使用<script>元素嵌入JavaScript代碼時,只須為<script>指定type屬性。然后,像下面這樣把JavaScript代碼直接放在元素內部即可:
<script type="text/javascript">
function sayHi(){
alert("Hi!");
}
</script>
如果沒有指定script屬性,則其默認值為text/javascript。
包含在<script>元素內部的JavaScript代碼將被從上至下依次解釋。在解釋器對<script>元素內部的所有代碼求值完畢以前,頁面中的其余內容都不會被瀏覽器加載或顯示。
在使用<script>嵌入JavaScript代碼的過程中,當代碼中出現"</script>"字符串時,由于解析嵌入式代碼的規則,瀏覽器會認為這是結束的</script>標簽。可以通過轉義字符“\”寫成<\/script>來解決這個問題。
1.2 包含外部 JavaScript 文件
外部JavaScript是將JavaScript代碼放在單獨的.js文件中,然后在HTML文檔中通過<script>標簽的src屬性引用這個文件。這種方法可以使代碼更加模塊化,便于重用和共享。
如果要通過<script>元素來包含外部JavaScript文件,那么src屬性就是必需的。這個屬性的值是一個指向外部JavaScript文件的鏈接。
<script type="text/javascript" src="example.js"></script>
與解析嵌入式JavaScript代碼一樣,在解析外部JavaScript文件(包括下載該文件)時,頁面的處理也會暫時停止。
注意:帶有src屬性的<script>元素不應該在其<script>和</script>標簽之間再包含額外的JavaScript代碼。如果包含了嵌入的代碼,則只會下載并執行外部腳本文件,嵌入的代碼會被忽略。
通過<script>元素的src屬性還可以包含來自外部域的JavaScript文件。它的src屬性可以是指向當前HTML頁面所在域之外的某個域中的完整URL。
<script type="text/javascript" src="http://www.somewhere.com/afile.js"></script>
于是,位于外部域中的代碼也會被加載和解析。
1.3 標簽的位置
在HTML中,所有的<script>標簽會按照它們出現的先后順序被解析。在不使用defer和async屬性的情況下,只有當前面的<script>標簽中的代碼解析完成后,才會開始解析后面的<script>標簽中的代碼。
通常,所有的<script>標簽應該放在頁面的<head>標簽中,這樣可以將外部文件(包括CSS和JavaScript文件)的引用集中放置。
然而,如果將所有的JavaScript文件都放在<head>標簽中,會導致瀏覽器在呈現頁面內容之前必須下載、解析并執行所有JavaScript代碼,這可能會造成明顯的延遲,導致瀏覽器窗口在加載過程中出現空白。
為了避免這種延遲問題,現代Web應用程序通常會將所有的JavaScript引用放置在<body>標簽中的頁面內容的后面。這樣做可以確保在解析JavaScript代碼之前,頁面的內容已經完全呈現在瀏覽器中,從而加快了打開網頁的速度。
JavaScript 解析過程包括兩個階段:預處理(也稱預編譯)和執行。
1、執行過程
HTML 文檔在瀏覽器中的解析過程是:按照文檔流從上到下逐步解析頁面結構和信息。
JavaScript 代碼作為嵌入的腳本應該也算做 HTML 文檔的組成部分,所以 JavaScript 代碼在裝載時的執行順序也是根據 <script> 標簽出現的順序來確定。
你是不是厭倦了一成不變的編程模式?想要突破自我,挑戰新技術想要突破自我,挑戰新技術?卻遲遲找不到可以練手的項目實戰?是不是夢想打造一個屬于自己的支付系統?那么,恭喜你,云端源想免費實戰直播——《微實戰-使用支付寶/微信支付服務,網站在線支付功能大揭秘》正在進行,點擊前往獲取源碼!云端源想
2、預編譯
當 JavaScript 引擎解析腳本時候,他會在與編譯期對所有聲明的變量和函數預先進行處理。當 JavaScript 解析器執行下面腳本時不會報錯。
alert(a); //返回值 undefined
var a=1;
alert(a); //返回值 1
由于變量聲明是在預編譯期被處理的,在執行期間對于所有的代碼來說,都是可見的,但是執行上面代碼,提示的值是 undefined 而不是 1。
因為變量初始化過程發生在執行期,而不是預編譯期。在執行期,JavaScript 解析器是按照代碼先后順序進行解析的,如果在前面代碼行中沒有為變量賦值,則 JavaScript 解析器會使用默認值 undefined 。
由于第二行中為變量 a 賦值了,所以在第三行代碼中會提示變量 a 的值為 1,而不是 undefined。
fun(); //調用函數,返回值1
function fun(){
alert(1);
}
函數聲明前調用函數也是合法的,并能夠正確解析,所以返回值是 1。但如果是下面這種方式則 JavaScript 解釋器會報錯。
fun(); //調用函數,返回語法錯誤
var fun=function(){
alert(1);
}
上面的這個例子中定義的函數僅作為值賦值給變量 fun 。在預編譯期,JavaScript 解釋器只能夠為聲明變量 fun 進行處理,而對于變量 fun 的值,只能等到執行期時按照順序進行賦值,自然就會出現語法錯誤,提示找不到對象 fun。
總結:聲明變量和函數可以在文檔的任意位置,但是良好的習慣應該是在所有 JavaScript 代碼之前聲明全局變量和函數,并對變量進行初始化賦值。在函數內部也是先聲明變量,后引用。
通過今天的分享,相信大家已經對JavaScript在HTML中的應用有了一定的了解。這只是冰山一角,JavaScript的潛力遠不止于此。希望這篇文章能激發大家對編程的熱情,讓我們一起在編程的世界里探索更多的可能性!
我們下期再見!
END
文案編輯|云端學長
文案配圖|云端學長
內容由:云端源想分享
了執行Javascript,需要在HTML文件內以特定的方式書寫JavaScript的代碼,JavaScript的書寫方法有多種,其執行的流程也各不相同:
此種嵌入方法無法操作<script>之后的DOM元素。因為<script>之后的DOM元素還未構造,因此在<script>標簽內就無法取得位于其后的DOM元素。
此種嵌入方法可以指定defer、async屬性。defer可以推遲執行,async可以異步執行。
此種嵌入方法在頁面讀取完后再對其執行,所以可以對所有的DOM元素操作。
<body onload="alert('hello')">
window.onload=function(){alert('hello');};
當window.onload事件觸發時,頁面上所有的DOM、樣式表、腳本、圖片、flash都已經加載完成了。
//window.onload不能同時編寫多個。
//以下代碼無法正確執行,結果只輸出第二個。
window.onload=function(){
alert("test1");
};
window.onload=function(){
alert("test2");
};
//$(document).ready()能同時編寫多個
//結果兩次都輸出
$(document).ready(function(){
alert("Hello World");
});
$(document).ready(function(){
alert("Hello again");
});
window.onload和body中onload也有些許區別:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.0.js"></script>
<script language="javascript">
window.onload=haha;
function haha(){console.log("window.onload");}
if(document.addEventListener){
function DOMContentLoaded(){
console.log("DOMContentLoaded");
}
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
}</script>
</head>
<body onload="console.log('bodyonload');">
<div id="div1">a</div>
</body>
</html>
在IE10和FireFox下,結果為 :
"DOMContentLoaded"
"bodyonload"
說明body中的onload會覆蓋window.onload
在chrome下,結果為:
DOMContentLoaded
window.onload
bodyonload
然后,如果把javascript代碼移到最下面,結果又會是什么樣呢?
chrome和IE10、FireFox的結果竟然是一樣的:
DOMContentLoaded
window.onload
IE 10、Fire Fox可以理解,window.on load和body中的 on load 誰在下面就是誰覆蓋誰,只會執行后面的那個。
onload方法可能需要等待時間,而本方法可以在完成HTML解析后發生的事件,減少等待時間。
在chrome、IE10和FireFox中,執行結果是:DOMContentLoaded然后才是onload的輸出。所以說一般情況下,DOMContentLoaded事件要在window.onload之前執行,當DOM樹構建完成的時候就會執行DOMContentLoaded事件。
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<script type="text/javascript" src="jquery2.js"></script>
<script language="javascript">
window.onload=haha;
function haha(){console.log(document.getElementById("div1"));}
if(document.addEventListener){
function DOMContentLoaded(){
console.log("DOMContentLoaded");
}
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
}
</script>
</head>
<body>
<div id="div1">a</div>
</body>
</html>
如果你是個jQuery使用者,你可能會經常使用$(document).ready();或者$(function(){}),這都是使用了DOMContentLoaded事件
5.1 使用原生js方法
動態創建script標簽,并指定script的src屬性
function loadJs(url, callback) {
var script=document.createElement('script');
script.type="text/javascript";
if (typeof(callback) !="undefined") {
if (script.readyState) {
script.onreadystatechange=function() {
if (script.readyState=="loaded" || script.readyState=="complete") {
script.onreadystatechange=null;
callback();
}
}
} else {
script.onload=function() {
callback();
}
}
}
script.src=url;
document.body.appendChild(script);
}
loadJs("test.js", function() {
alert('done');
});
還可以使用同樣的原理動態加載css文件,只不過插入的的父節點是head標簽。
5.2 使用document.write/writeln()方式
該種方式可以實現js文件的動態加載,原理就是在重寫文檔流,這種方式會導致整個頁面重繪。
document.writeln("<script src=\"http://lib.sinaapp.com/js/jquery/1.6/jquery.min.js\"></script>");
需要注意的是特殊字符的轉義。
5.3 使用jQuery
使用getScript(url,callback)方法實現動態加載js文件
$.getScript('test.js',function(){
alert('done');
});
-End-
*請認真填寫需求信息,我們會在24小時內與您取得聯系。