摘要:為什么會這樣這段代碼究竟是如何運行的執行上下文堆棧瀏覽器中的解釋器單線程運行。瀏覽器始終執行位于堆棧頂部的,并且一旦函數完成執行當前操作,它將從堆棧頂部彈出,將控制權返回到當前堆棧中的下方上下文。確定在上下文中的值。
原文:What is the Execution Context & Stack in JavaScript?
git地址:JavaScript中的執行上下文和隊列(棧)的關系?
導讀:以前總是看到相關文章提到什么變量提升,函數提升啥的,什么函數提升優先級大于變量的,總是知其然,不知其所以然,當面試官拿著同一name,卻不斷function, 和var賦值,然后讓你告訴他每一個階段該是什么值的時候,拿著啥變量提升和函數提升是解釋不通的,至少我不能-_-。David Shariff的這篇文章為我們講述了其中的原理,讓人看了豁然開朗
在這篇文章中,我將深入探討JavaScript的一個最基本的部分,執行上下文。 在本文結束時,您會更清楚解釋器都做了些什么,以至于某些函數、變量在聲明它們之前就可以使用,它們的值是如何確定的。什么是執行上下文?
當代碼在JavaScript中運行時,它的執行環境非常重要,并且它們分為以下幾類:
global 代碼 -- 首次執行代碼的默認環境
function 代碼 -- 每當執行流程進入函數體時
Eval 代碼 -- 要在內部eval 函數內執行的文本
為了便于理解,本文中執行上下文是指:當前被執行的代碼的環境、作用域;接下來讓我們看一個執行上下文中包含global、function content的代碼:
這里沒有什么特別之處,1個global context由紫色邊框表示,3個不同的function contexts分別由綠色、藍色和橙色邊框表示。只能有1個global context,可以從程序中的任何其他上下文訪問。
您可以擁有任意數量的function contexts,并且每個函數調用都會創建一個新的上下文,從而創建一個私有作用域,在該作用域內,無法從當前函數作用域外直接訪問函數內部聲明的任何內容。在上面的示例中,函數可以訪問在其當前上下文之外聲明的變量,但外部上下文無法訪問在其內部聲明的變量/函數。為什么會這樣?這段代碼究竟是如何運行的?
執行上下文堆棧瀏覽器中的JavaScript解釋器單線程運行。這就意味著同一時間瀏覽器只執行一件事,其它的事件在執行隊列中排隊。下圖是單線程隊列的抽象視圖:
我們已經知道,當瀏覽器首次加載您的腳本時,它默認進入全局執行上下文(global execution contenrt)。如果在您的全局代碼中調用一個函數,程序的順序流進入被調用的函數,創建一個新函數execution context并將該上下文推送到頂部execution stack(執行隊列)。
如果在當前函數中調用另一個函數,則會發生同樣的事情。代碼的執行流程進入內部函數,該函數創建一個execution context并推送到執行隊列的頂部。瀏覽器始終執行位于堆棧頂部的execution context,并且一旦函數完成執行當前操作execution context,它將從堆棧頂部彈出,將控制權返回到當前堆棧中的下方上下文。下面的例子顯示了一個遞歸函數和程序execution stack:
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
代碼只調用自身3次,將i的值遞增1.每次調用foo函數時,都會創建一個新的執行上下文。一旦執行完成,它就會彈出堆棧并且將控制權交給它下面的上下文,直到再次到達global context(koa2的洋蔥圖想到了沒?)
以下是執行隊列的5個關鍵點:單線程、
同步執行
全局上下文
無限級的函數上下文
每個函數調用都會創建一個新的執行上下文(execution context),包括對自身的調用(遞歸)
執行上下文詳情所以我們現在知道每次調用函數時都會創建一個新的執行上下文(execution context) 。但是,在JavaScript解釋器中,每次調用生成執行上下文(execution context)都有兩個階段:
創建階段 [調用函數時,但在執行任何代碼之前]:
創建作用域鏈。
創建變量(variables),函數(functions )和參數(arguments)
確定"this"。
激活/執行階段:
var 賦值,(function聲明)指向函數,解釋/執行代碼
可以將每個execution context概念上表示為具有3個屬性的對象:
executionContextObj = { "scopeChain": { /* variableObject + all parent execution context"s variableObject */ }, "variableObject": { /* function arguments / parameters, inner variable and function declarations */ }, "this": {} }激活/變量對象[AO / VO]
這executionContextObj是在調用函數時,但在執行實際函數之前創建的。這是第一階段:創建階段。這里,解釋器通過掃描傳入的參數或arguments、本地函數聲明和局部變量聲明來創建executionContextObj。這次掃描的結果就變成了executionContextObj.variableObject。
以下是解釋器如何解析代碼的偽概述:遇到函數調用。
在執行function代碼之前,創建執行上下文(execution context)。
進入創建階段:
初始化作用域鏈(Scope Chain)。
創建變量對象(variable object):
創建arguments object,檢查參數的上下文,初始化名稱和值并創建引用副本。
掃描上下文以獲取函數聲明:
對于找到的每個函數,在variable object中創建一個以函數名稱為屬性的鍵值對,值指向內存中函數的引用指針。
如果函數名已存在,則將覆蓋引用指針值。
掃描上下文以獲取變量聲明:
對于找到的每個變量聲明,在variable object中創建一個以變量名為屬性的鍵值對,值初始化為undefined。
如果變量名已經存在于variable object,則不執行任何操作并繼續掃描。
確定"this"在上下文中的值。
激活/執行階段:
在上下文中運行/解析函數體的代碼,并在代碼逐行執行時為變量賦值。
我們來看一個例子:
function foo(i) { var a = "hello"; var b = function privateB() { }; function c() { } } foo(22);
在調用時foo(22),creation stage長這樣子:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this: { ... } }
正如您所看到的,creation stage定義屬性的name,不為它們賦值,但formal arguments / parameters(函數傳參,arguments)除外。一旦creation stage完成后,執行流程進入函數體,在函數已經完成執行之后的execution stage如下:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: "hello", b: pointer to function privateB() }, this: { ... } }提升
在很多JavaScript的資料中都提到了提升,解釋變量和函數聲明被提升到其作用域的頂部。但是,沒有人詳細解釋為什么會發生這種情況,而在你掌握了關于解釋器如何創建activation object后,會很容易理解。示例:
(function() { console.log(typeof foo); // function pointer console.log(typeof bar); // undefined var foo = "hello", bar = function() { return "world"; }; function foo() { return "hello"; } }());?
我們現在可以回答的問題是:
為什么我們可以在聲明它之前訪問foo?
如果我們遵循creation stage,我們知道變量在activation / code execution stage之前就創建了。所以當功能流程開始執行時,foo早就在activation object中定義了。
foo是聲明了兩次,為什么顯示foo的是 function ,__不是__ undefined 或 string?
即使foo聲明了兩次,我們也知道在creation stage函數在變量之前就在activation objectbefore上創建了,如果屬性名已經存在于activation object,解釋器會忽略掉此次聲明。
因此,首先會在activation object上創建一個foo()的引用,當解釋器到達時var foo,屬性名稱foo存在,所以代碼什么也不做,然后繼續。
為什么 bar 是 undefined?
bar實際上是一個具有函數賦值的變量,我們知道變量是在creation stage創建的,但它們的初始值為undefined。
概要希望到現在您已經很好地掌握了JavaScript解釋器如何執行您的代碼。理解執行上下文和隊列可以讓您了解代碼沒有達到預期的原因
您是否認為了解解釋器的內部工作原理是您的JavaScript知識的重要組成部分?知道執行上下文的每個階段是否有助于您編寫更好的JavaScript?
__注意__:有些人一直在問關于閉包,回調,超時等,我將在在下一篇文章中涉及,主要概述作用域鏈與execution context的關系。
拓展ECMA-262-3 in detail. Chapter 2. Variable object
Identifier Resolution, Execution Contexts and scope chains
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97984.html
摘要:調用棧是一種棧結構它用來存儲計算機程序執行時候其活躍子程序的信息。調用棧是解析器的一種機制。那是如何處理處理函數的調用關系的答案是調用棧。主線程之外存在一個任務隊列異步任務有了運行結果會在任務隊列之中放置一個任務。 1:基本概念 棧(stack):用來保存簡單的數據字段。 堆(heap):用來保存棧中簡單的數據字段對指針的引用。 隊列:是一種先進先出的線性數據結構。 函數的調用的進棧和...
摘要:中有三種數據結構棧堆隊列。前端進擊的巨人一執行上下文與執行棧,變量對象中解釋執行棧時,舉了一個乒乓球盒子的例子,來演示棧的存取方式,這里再舉個栗子搭積木。對于基本類型,棧中存儲的就是它自身的值,所以新內存空間存儲的也是一個值。 面試經常遇到的深淺拷貝,事件輪詢,函數調用棧,閉包等容易出錯的題目,究其原因,都是跟JavaScript基礎知識不牢固有關,下層地基沒打好,上層就是豆腐渣工程,...
摘要:當前函數執行完成后,當前函數的執行上下文出棧,并等待垃圾回收。作用域與作用域鏈到來有全局作用域函數作用域和塊級作用域新增。 引言 Javascript是前端面試的重點,本文重點梳理下 Javascript 中的常考知識點,然后就一些容易出現的題目進行解析。限于文章的篇幅,無法將知識點講解的面面俱到,本文只羅列了一些重難點,如果想要了解更多內容歡迎點擊我的博客。 一、變量類型 1.JS ...
摘要:當前函數執行完成后,當前函數的執行上下文出棧,并等待垃圾回收。作用域與作用域鏈到來有全局作用域函數作用域和塊級作用域新增。 引言 Javascript是前端面試的重點,本文重點梳理下 Javascript 中的常考知識點,然后就一些容易出現的題目進行解析。限于文章的篇幅,無法將知識點講解的面面俱到,本文只羅列了一些重難點,如果想要了解更多內容歡迎點擊我的博客。 一、變量類型 1.JS ...
閱讀 1002·2021-09-30 09:58
閱讀 2829·2021-09-09 11:55
閱讀 2001·2021-09-01 11:41
閱讀 991·2019-08-30 15:55
閱讀 3350·2019-08-30 12:50
閱讀 3495·2019-08-29 18:37
閱讀 3295·2019-08-29 16:37
閱讀 2011·2019-08-29 13:00