Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
咱們開始之前,有個問題大家可以一起討論: JS是解釋語言還是編譯語言?
(JS)是一種解釋語言,有自己的編譯器形式,運行在JS引擎中。
每個web瀏覽器都有自己的JS引擎形式,盡管目的一樣。Chrome 有 v8, Mozilla 有 spider monkey等,JS引擎只是將JS源代碼轉換成編譯器能夠理解的語言,然后執行它。
JS 代碼運行的環境構成了執行上下文,執行上下文決定哪段代碼可以訪問變量、函數、對象等。
任何時候碼第一次運行,或者當代碼不在任何函數中時,它都會進入全局執行上下文。在整個代碼執行過程中只有一個全局執行上下文。
對于瀏覽器全局執行上下文,它做兩件事:
當函數執行時,它就創建一個新的函數執行上下文,可以有任意數量的函數執行上下文。
瀏覽器中JS解器是單線程,同一時間只能干一件事。代碼中有一個全局的執行上下文和無數個函數執行上下文,那么他們是按什么順序執行的呢?
這里就需要一個 執行上下文棧 的概念了,JS引擎是通過創建執行上下文棧來管理執行上下文的。這里可以把執行上下文棧描述為一個存著函數調用的棧結構,執行順序遵循先進后出的原則,也就是說一個函數的執行上下文,在函數執行完畢之后,會被移除執行上下文棧。
每當腳本在瀏覽器中加載時,堆棧中的第一個元素就是全局執行上下文。然而,當一個函數執行時,將創建一個執行上下文,并將其虛擬的放置在全局執行上下文之上。函數一旦執行完畢,就會從執行堆棧中彈出并將控制權交給到它下面的上下文中。
咱們舉個例子,來模擬上述的過程:
步驟1:當上述代碼加載到瀏覽器中時,JS引擎創建一個全局執行上下文(global execution context )并將其推入當前執行堆棧。
步驟2:假設最后執行func1()調用,然后JS引擎為該函數創建一個新的執行上下文(function execution context),并將其推到全局執行上下文的頂部。
步驟3:在func1()中,咱們調用了func2(),因此JS引擎為該函數創建一個新的執行上下文,并將其推到func1()執行上下文的頂部。
步驟4:當func2()函數結束時,它的執行上下文從當前堆棧中彈出,控制權交給它下面的執行上下文,即func1()函數的執行上下文。
步驟5:當func1()函數結束時,它的執行堆棧將從堆棧中刪除,控制權交給全局執行上下文。執行完所有代碼后,JS 引擎將從當前堆棧中刪除全局執行上下文。
執行上下文主要有兩個階段。
函數創建時做的三件事:
1.首先,為用域鏈內的每個函數或變量構建到外部環境的連接。告訴執行上下文它應該包含什么,以及它應該在哪里查找解析函數引用和變量值的方法。
2.接著,通過掃描作用鏈,創建一個環境記錄,其中全局上下文的創建和引用(web瀏覽器中的window)、變量、函數和函數參數都在內存中完成。
3.最后,在第一步中創建的每個執行上下文中確定this的值(對于全局執行上下文,this指向的是window)。
因此,咱們可以將創建階段表示為
創建階段 = {
scopeChain: {
/* 作用域鏈解析 */
},
variableObject: {
/* arguments, 函數參數, 內部變量 等等*/
},
this: {},
}
variableObject: 初始化函數的參數variableObject,提升函數聲明和變量聲明。
scopeChain: 在執行上下文的創建階段,作用域會在變量對象創建之后創建。作用域鏈本身包括變量對象。作用域負責解析變量,當被要求解析變量的時候,會從代碼嵌套結構的最內層開始,如果在最內層沒有找到對應變量,則依次向父級作用域中進行查找,直到尋找到最外層作用域。
this: 確定this的指向,這里需要注意的事this的值是在執行的時候確定的,在定義的時候并不會確定。
這是代碼開始在創建階段形成的執行上下文中運行的階段,并逐行分配變量值。
在執行開始時,JS 引擎在其創建階段對象中尋找執行函數的引用。如果不能在自己的作用域內找到它,它將繼續向上查找,直到到達全局環境。
如果在全局環境中沒有找到引用,它將返回一個錯誤。但是,如果找到了一個引用,并且函數執行正確,那么這個特定函數的執行上下文將從堆棧中彈出,JS 引擎將移動到下一個函數,在那里,它們的執行上下文將被添加到堆棧中并執行,依此類推。
咱們通過示例查看上面的兩個階段,以便更好地理解它。
1111
在創建階段,全局執行上下文類似于這樣
globalExecutionObj = {
outerEnvironmentConnection: null,
variableObjectMapping: {
name: uninitialized,
title: undefined,
date: uninitialized,
func1: func,
},
this: window //Global Object
}
**注意:**上面,let (name)和const (date)定義的變量在創建階段沒有任何關聯的值,但是var (title)定義的變量會被設置為undefined。
這就是為什么咱們可以在聲明var定義的變量之前訪問它們**(雖然沒有定義)**,但是在聲明let和 const變量之前訪問它們時,會得到一個引用錯誤。
這就是咱們所說的變量提升,即所有使用var的變量聲明都被提升它們的局部作用域(在函數內部聲明)或者全局作用域的頂部(在函數外部聲明的)。
在執行階段,完成變量分配。所以全局執行上下文在執行階段類似如下:
globalExectutionObj = {
outerEnvironmentConnection: null,
variableObjectMapping: {
name: "overflowjs.com",
title: "Execution context",
date: "5 july 2019",
func1: pointer to function func1,
},
this: window //Global Object
}
**注意:**在執行階段,如果JS引擎在源代碼中聲明位置找不到let變量的值,那么它將為其賦值undefined。
現在,當func1執行,就會生成一個新的函數執行上下文,其創建階段類似如下:
func1ExecutionObj = {
outerEnvironmentConnection: Global,
variableObjectMapping: {
arguments: {
0: 10,
length: 1
},
num: 10,
author: undefined,
val: uninitialized,
func2: undefined
fixed: uninitialized
addFive: pointer to function addFive()
},
this: Global Object or undefined
}
在執行階段類似如下:
func1ExecutionObj = {
outerEnvironmentConnection: Global,
variableObjectMapping: {
arguments: {
0: 10,
length: 1
},
num: 10,
author: "Deepak",
val: 3,
func2: pointer to function func2()
fixed: "Divine"
addFive: pointer to function addFive()
},
this: Global Object or undefined
}
函數執行完成后,將更新全局環境。然后全局代碼完成,程序結束。
JavaScript中的作用域分為三種:
作用域最大的作用就是隔離變量或函數,并控制他們的生命周期。作用域是在函數執行上下文創建的時候定義好的,不是在函數執行的時候定義的。
當一個塊或者函數嵌套在另一個塊或函數中時,就發生了作用域的嵌套。在當前函數中如果JS引擎無法找到某個變量,就會往上級嵌套的作用域中去尋找,直到找到該變量或抵達全局作用域,這樣的鏈式關系成為作用域鏈(Scope Chain)。
來個例子演示一下:
var scope = 'global';
function checkscope(s) {
var scope = 'local scope';
function f() {
return scope;
}
return f();
}
checkScope('scope');
首先在checkscope函數聲明的時候,內部會綁定一個[[scope]]的內部屬性:
checkscope.[[scope]] = [
globalContext.VO
];
接著在checkScope函數在執行之前,創建執行上下文checkscopeContext,并推入執行上下文棧:
// -> 初始化作用域鏈;
checkscopeContext = {
scope: [globalContext.VO],
}
// -> 創建變量對象
checkscopeContext = {
scope: [globalContext.VO],
VO = {
arguments: {
0: 'scope',
length: 1,
},
s: 'scope', // 傳入的參數
f: function f(),
scope: undefined, // 此時聲明的變量為undefined
},
}
// -> 將變量對象壓入作用域鏈的最頂端
checkscopeContext = {
scope: [VO, globalContext.VO],
VO = {
arguments: {
0: 'scope',
length: 1,
},
s: 'scope', // 傳入的參數
f: function f(),
scope: undefined, // 此時聲明的變量為undefined
},
}
執行階段,修改變量對象里面對應字段的值:
checkscopeContext = {
scope: [VO, globalContext.VO],
VO = {
arguments: {
0: 'scope',
length: 1,
},
s: 'scope', // 傳入的參數
f: pointer to function f(),
scope: 'local scope', // 變量賦值
}
}
在代碼執行階段,會看到f函數的聲明代碼,給f函數綁定[[scope]]屬性:
f.[[scope]] = [
checkscopeContext.VO, // f函數的作用域還包括checkscope的變量對象
globalContext.VO
];
文本到這就結束了,希望對大伙有所幫助。
作者:DEEPAK GUPTA 譯者:前端小智 來源:overflowjs.com
原文:https://overflowjs.com/posts/Javascript-Execution-Context-and-Hoisting.html
篇文章的目的是為了讓你徹底理解 JavaScript 的執行,如果你到本文最后還沒有理解,你可以揍我一頓。
無論你是 JavaScript 新手還是老手,無論你是在面試工作,還是只是做常規的開發工作,通常會發現給定幾行代碼,你需要知道要輸出什么以及以什么順序輸出 . 由于 JavaScript 是一種單線程語言,我們可以得出以下結論:
let a = '1';
console.log(a);
let b = '2';
console.log(b);
然而,JavaScript 實際上是這樣的:
setTimeout(function(){
console.log('start')
});
new Promise(function(resolve){
console.log('start for');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('start then')
});
console.log('end');
// Following the idea that JS executes in the order in which the statements appear, I confidently write down the output:
// start
// start for
// start then
// end
在 Chrome 上查看它是完全錯誤的
JavaScript 是一種單線程語言。Web-worker 是在最新的 HTML5 中提出的,但 JavaScript 是單線程的核心保持不變。所以所有 JavaScript 版本的“多線程”都是用單線程模擬的,所有的 JavaScript 多線程都是紙老虎!
由于 JavaScript 是單線程的,它就像一個只有一個窗口的銀行。客戶需要一一排隊辦理業務。
同樣,JavaScript 任務也需要一個一個地執行。如果一項任務花費的時間太長,則下一項也必須等待。
那么問題來了,如果我們想瀏覽新聞,但新聞中包含加載緩慢的超高清圖像,我們的網頁是否應該一直卡住直到圖像完全顯示?所以聰明的程序員將任務分為兩類:
當我們打開一個網站時,頁面的渲染過程是很多同步任務,比如渲染頁面骨架和頁面元素。
需要大量時間的任務,比如加載圖片和音樂,都是異步任務。這部分有嚴格的文字定義,但本文的目的是以最小的學習成本徹底理解實現機制,所以我們用一張圖來說明:
文字要表達的內容:
同步和異步任務去不同的執行“地方”,同步任務去主線程,異步任務去事件表和注冊函數。
當指定的事件完成時,事件表將此函數移至事件隊列。
如果執行后主線程中的任務為空,事件隊列會讀取相應的函數,進入主線程執行。
這個過程一遍又一遍地重復,稱為事件循環。
我們怎么知道主線程棧是空的?JavaScript 引擎有一個監控進程,不斷檢查主線程堆棧是否為空,如果是,則檢查 Event Queue 以查看是否有任何函數等待調用。
說了這么多,不如直接寫一段代碼:
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('success!');
}
})
console.log('end');
這是一個簡單的ajax請求代碼:
相信通過上面的文字和代碼,你對JS的執行順序有了初步的了解。接下來,我們來看看進階話題:setTimeout。
著名的 setTimeout 無需進一步解釋。setTimeout 的第一印象是異步執行可以延遲,我們經常這樣實現:
setTimeout(() => {
console.log(‘Delay 3 seconds’);
},3000)
當 setTimeout 用得越來越多時,問題也出現了。有時函數會在 3 秒的書面延遲后 5 或 6 秒內執行。怎么了?
讓我們從一個例子開始:
setTimeout(() => {
task();
},3000)
console.log('console');
按照我們之前的結論,setTimeout是異步的,應該先執行console.log。
//console
//task()
去看看吧!這是正確的!然后我們修改之前的代碼:
復制setTimeout(() => {
task()
},3000)
sleep(10000000)
控制臺上的 task() 在 Chrome 中執行需要超過 3 秒的時間。
此時,我們需要重新思考setTimeout的定義。
先說上面的代碼是如何執行的:
上述過程完成后,我們知道setTimeout是一個在指定時間后將任務添加到Event Queue(本例中為task())的函數。
而且,由于是單線程任務,需要一個一個執行,如果上一個任務耗時過長,我們只能等待。導致實際延遲超過 3 秒。
SetTimeout(fn,0) 是我們經常遇到的另一個代碼。可以立即完成嗎?
SetTimeout (fn,0) 指定任務將在主線程上最早可用的空閑時間執行。這意味著一旦堆棧中的所有同步任務完成并且堆棧為空,主線程將立即執行。例如:
//code1
console.log('one');
setTimeout(() => {
console.log('two')
},0);
// result
// one
// two
//code2
console.log('one');
setTimeout(() => {
console.log('two')
},3000);
// result
// one
// ... 3s later
// two
關于 setTimeout 要補充的一點是,即使主線程是空的,0 毫秒實際上也是無法到達的。根據 HTML 標準,最小值為 4 毫秒。有興趣的同學可以自行了解。
說了 setTimeout,你不能錯過它的孿生兄弟 setInterval。它們是相似的,只是后者是循環執行。對于執行順序,setInterval 將按指定的時間間隔將注冊的函數放入事件隊列中。如果上一個任務耗時過長,也需要等待。
唯一需要注意的是,對于 setInterval(fn,ms),我們已經知道不是每 ms 秒執行一次 fn,而是每 ms 秒進入 Event Queue。一旦 setInterval 的回調 fn 花費的時間超過了延遲 ms,時間間隔就完全不可見了。請讀者細細品味這句話。
我們已經看過傳統的計時器,然后,我們將探討 Promise 與 process.Nexttick(回調)的性能。
Promise 的定義和功能這里就不介紹了,process.nexttick(回調)類似于node.js 版本的“setTimeout”,在事件循環的下一次迭代中調用回調函數。
我們開始談正事吧。除了廣義的同步和異步任務,我們對任務有更詳細的定義:
不同類型的任務會進入對應的Event Queue。例如,setTimeout 和 setInterval 將進入同一個事件隊列。
事件循環的順序決定了 JS 代碼的執行順序。輸入整體代碼(宏任務)后,第一個循環開始。然后,執行所有微任務。然后再從宏任務開始,找一個任務隊列完成,然后,執行所有的微任務。如果聽起來有點繞,我們用本文開頭的代碼來說明:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
事件循環、宏任務和微任務的關系如下圖所示:
讓我們看一些更復雜的代碼,看看你是否真的了解 JS 的工作原理:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
第一輪事件循環流程分析如下:
整個腳本作為第一個宏任務進入主線程,遇到console.log,打印1。
好了,第一輪事件循環正式結束,本輪結果輸出1,7,6,8。所以第二個時間循環從 setTimeout1 宏任務開始:
首先,print2。接下來是process.nexttick(),它也被分派到微任務事件隊列中,稱為process2。新的 Promise 立即執行輸出 4,然后也被分發到微任務事件隊列中,記為 then2。
整個代碼,一共經過了3次事件循環,完整的輸出為1,7,6,8,2,4,3,5,9,11,10,12。
我們從一開始就說過 JavaScript 是單線程語言,無論什么新的框架和語法實現被稱為異步,實際上都是以同步的方式模擬的,所以牢牢掌握單線程很重要。
事件循環是實現異步 JavaScript 的一種方法,也是 JavaScript 的執行機制。
在 Node.js、瀏覽器、Ringo 等不同的環境中執行和運行 JavaScript 是有很大區別的。雖然運行多指 JavaScript 解析引擎,但它是統一的。
還有許多其他類型的微任務和宏任務,例如 setImmediate,它們不進行中介。
JavaScript 是一種單線程語言。事件循環是 JavaScript 的執行機制。
牢牢把握兩個基本點,以認真學習JavaScript為中心,早日實現成為前端高手的偉大夢想!
來源: WEB前端開發社區
言
在JavaScript的世界里,事件循環(Event Loop)是一個核心概念,它決定了JavaScript代碼的執行順序,尤其是異步代碼。理解事件循環對于編寫高效、響應迅速的JavaScript程序至關重要。本文將深入探討事件循環的原理,并通過實際代碼示例展示其在JavaScript編程中的應用。
1. 理解事件循環
1.1 JavaScript的執行模型
JavaScript有一個基于單線程的事件循環執行模型。這意味著JavaScript代碼在一個單獨的線程上執行,一次只能執行一個任務。為了處理高延遲操作(如I/O),JavaScript采用了異步編程模型。
1.2 任務隊列
JavaScript中的任務分為兩種:宏觀任務(macrotasks)和微觀任務(microtasks)。宏觀任務包括例如setTimeout、setInterval、I/O操作等。微觀任務則包括例如Promise的回調、MutationObserver等。
1.3 事件循環的工作流程
事件循環的工作流程大致如下:
2. 事件循環的實際應用
2.1 setTimeout和setInterval
示例代碼:
console.log('開始');
setTimeout(() => {
console.log('setTimeout');
}, 0);
console.log('結束');
輸出順序:開始 -> 結束 -> setTimeout
2.2 Promise和async/await
示例代碼:
console.log('開始');
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('結束');
輸出順序:開始 -> 結束 -> Promise
2.3 宏觀任務和微觀任務的交互
示例代碼:
console.log('開始');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => {
console.log('Promise 1');
});
}, 0);
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 2');
});
console.log('結束');
輸出順序:開始 -> 結束 -> Promise 2 -> setTimeout 1 -> Promise 1 -> setTimeout 2
3. 事件循環的性能考量
雖然事件循環使得JavaScript能夠高效地處理異步操作,但在編寫代碼時,應避免過多地使用微觀任務,特別是在性能敏感的應用中。此外,理解事件循環對于調試異步代碼也非常重要。
總結
事件循環是JavaScript中一個核心的概念,它決定了異步代碼的執行順序。通過理解事件循環,我們可以更有效地編寫和管理異步操作,從而創建響應迅速且高效的JavaScript程序。在實際應用中,無論是使用setTimeout、Promise,還是async/await,事件循環都扮演著至關重要的角色。然而,在享受事件循環帶來的便利的同時,也需要注意性能和代碼結構的問題,確保程序的高效運行和可維護性。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。