上下文的原意是 context
, 做用域的原意是scope
, 這兩個不是一個東西。javascript
每個函數的調用(function invocation) 都有對應的scope
和context
.java
scope
指的是 函數被調用的時候, 各個變量的做用區域context
指的是 current scope and its enclosing scope. 就是當前scope 和包裹它外面的scope. 若是一個變量在當前scope沒找到,那麼它會自底向上繼續找enclosing scope 直到找到爲止。很像javascript 的prototype那樣的找法。常常在javascript中,函數被調用的時候, 查看this
指向哪一個object
, 那麼那個object
就是當前的 "上下文"。瀏覽器
小明告訴小紅:「你放心吧,他答應你的條件了。」函數
在讀者的眼中,「他」是誰根本無從知曉,由於這句話缺乏「上下文」;this
從小強家裏出來後,小明告訴小紅:「你放心吧,他答應你的條件了。」spa
誰都知道,「他」指的是「小強」,由於有「上下文」。prototype
Javascript中代碼的運行環境分爲如下三種:線程
在網上能夠找到不少闡述做用域的資源,爲了使該文便於你們理解,咱們能夠將「執行上下文」看作當前代碼的運行環境或者做用域。下面咱們來看一個示例,其中包括了全局以及函數級別的執行上下文:3d
上圖中,一共用4個執行上下文。紫色的表明全局的上下文;綠色表明person函數內的上下文;藍色以及橙色表明person函數內的另外兩個函數的上下文。注意,無論什麼狀況下,只存在一個全局的上下文,該上下文能被任何其它的上下文所訪問到。也就是說,咱們能夠在person的上下文中訪問到全局上下文中的sayHello變量,固然在函數firstName或者lastName中一樣能夠訪問到該變量。code
至於函數上下文的個數是沒有任何限制的,每到調用執行一個函數時,引擎就會自動新建出一個函數上下文,換句話說,就是新建一個局部做用域,能夠在該局部做用域中聲明私有變量等,在外部的上下文中是沒法直接訪問到該局部做用域內的元素的。在上述例子的,內部的函數能夠訪問到外部上下文中的聲明的變量,反之則行不通。那麼,這究竟是什麼緣由呢?引擎內部是如何處理的呢?
在瀏覽器中,javascript引擎的工做方式是單線程的。也就是說,某一時刻只有惟一的一個事件是被激活處理的,其它的事件被放入隊列中,等待被處理。下面的示例圖描述了這樣的一個堆棧:
咱們已經知道,當javascript代碼文件被瀏覽器載入後,默認最早進入的是一個全局的執行上下文。當在全局上下文中調用執行一個函數時,程序流就進入該被調用函數內,此時引擎就會爲該函數建立一個新的執行上下文,而且將其壓入到執行上下文堆棧的頂部。瀏覽器老是執行當前在堆棧頂部的上下文,一旦執行完畢,該上下文就會從堆棧頂部被彈出,而後,進入其下的上下文執行代碼。這樣,堆棧中的上下文就會被依次執行而且彈出堆棧,直到回到全局的上下文。請看下面一個例子:
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
上述foo被聲明後,經過()運算符強制直接運行了。函數代碼就是調用了其自身3次,每次是局部變量i增長1。每次foo函數被自身調用時,就會有一個新的執行上下文被建立。每當一個上下文執行完畢,該上上下文就被彈出堆棧,回到上一個上下文,直到再次回到全局上下文。真個過程抽象以下圖:
因而可知 ,對於執行上下文這個抽象的概念,能夠概括爲如下幾點:
咱們如今已經知道,每當調用一個函數時,一個新的執行上下文就會被建立出來。然而,在javascript引擎內部,這個上下文的建立過程具體分爲兩個階段:
實際上,能夠把執行上下文看作一個對象,其下包含了以上3個屬性:
(executionContextObj = { variableObject: { /* 函數中的arguments對象, 參數, 內部的變量以及函數聲明 */ }, scopeChain: { /* variableObject 以及全部父執行上下文中的variableObject */ }, this: {} }
確切地說,執行上下文對象(上述的executionContextObj)是在函數被調用時,可是在函數體被真正執行之前所建立的。函數被調用時,就是我上述所描述的兩個階段中的第一個階段 – 創建階段。這個時刻,引擎會檢查函數中的參數,聲明的變量以及內部函數,而後基於這些信息創建執行上下文對象(executionContextObj)。在這個階段,variableObject對象,做用域鏈,以及this所指向的對象都會被肯定。
上述第一個階段的具體過程以下:
進入第一個階段-創建階段:
每找到一個函數聲明,就在variableObject下面用函數名創建一個屬性,屬性值就是指向該函數在內存中的地址的一個引用
若是上述函數名已經存在於variableObject下,那麼對應的屬性值會被新的引用所覆蓋。
執行函數體中的代碼,一行一行地運行代碼,給variableObject中的變量屬性賦值。
下面來看個具體的代碼示例:
function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { } } foo(22);
在調用foo(22)的時候,創建階段以下:
fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, scopeChain: { ... }, this: { ... } }
因而可知,在創建階段,除了arguments,函數的聲明,以及參數被賦予了具體的屬性值,其它的變量屬性默認的都是undefined。一旦上述創建階段結束,引擎就會進入代碼執行階段,這個階段完成後,上述執行上下文對象以下:
fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: pointer to function privateB() }, scopeChain: { ... }, this: { ... } }
咱們看到,只有在代碼執行階段,變量屬性纔會被賦予具體的值。
在網上一直看到這樣的總結: 在函數中聲明的變量以及函數,其做用域提高到函數頂部,換句話說,就是一進入函數體,就能夠訪問到其中聲明的變量以及函數。這是對的,可是知道其中的原因嗎?相信你經過上述的解釋應該也有所明白了。不過在這邊再分析一下。看下面一段代碼:
(function() { console.log(typeof foo); // function pointer console.log(typeof bar); // undefined var foo = 'hello', bar = function() { return 'world'; }; function foo() { return 'hello'; } }());
上述代碼定義了一個匿名函數,而且經過()運算符強制理解執行。那麼咱們知道這個時候就會有個執行上下文被建立,咱們看到例子中立刻能夠訪問foo以及bar變量,而且經過typeof輸出foo爲一個函數引用,bar爲undefined。
爲何咱們能夠在聲明foo變量之前就能夠訪問到foo呢?
由於在上下文的創建階段,先是處理arguments, 參數,接着是函數的聲明,最後是變量的聲明。那麼,發現foo函數的聲明後,就會在variableObject下面創建一個foo屬性,其值是一個指向函數的引用。當處理變量聲明的時候,發現有var foo的聲明,可是variableObject已經具備了foo屬性,因此直接跳過。當進入代碼執行階段的時候,就能夠經過訪問到foo屬性了,由於它已經就存在,而且是一個函數引用。
爲何bar是undefined呢?
由於bar是變量的聲明,在創建階段的時候,被賦予的默認的值爲undefined。因爲它只要在代碼執行階段纔會被賦予具體的值,因此,當調用typeof(bar)的時候輸出的值爲undefined。
這樣差很少對執行上下文有所理解了吧,大佬們但願能給出更好地建議