帶你完全搞懂執行上下文

執行上下文

執行上下文 能夠理解爲當前代碼的執行環境,同一個函數在不一樣的環境中執行,會由於訪問數據的不一樣產生不同的結果。前端

執行上下文分爲三種:數組

  • 全局執行上下文:只有一個,程序首次運行時建立,它會在瀏覽器中建立一個全局對象(window對象),使this指向這個全局對象
  • 函數執行上下文:函數被調用時建立,每次調用都會爲該函數建立一個新的執行上下文
  • Eval 函數執行上下文:運行eval函數中的代碼時建立的執行上下文,少用且不建議使用

執行上下文棧

執行上下文棧(Execution context stack,ECS),也叫函數調用棧(call stack),是一種擁有 LIFO(後進先出)數據結構的棧,用於存儲代碼執行時建立的執行上下文瀏覽器

因爲JS是單線程的,每次只能作一件事情,經過這種機制,咱們可以追蹤到哪一個函數正在執行,其餘函數在調用棧中排隊等待執行。數據結構

JS引擎第一次執行腳本時,會建立一個全局執行上下文壓到棧頂,而後隨着每次函數的調用都會建立一個新的執行上下文放入到棧頂中,隨着函數執行完畢後被執行上下文棧頂彈出,直到回到全局的執行上下文中。閉包

代碼實例app

var color = 'blue';

function changeColor() {
  var anotherColor = 'red';

  function swapColors() {
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
  }

  swapColors();
}

changeColor();

console.log(color); // red

執行過程能夠在 devToolcall stack 中看到,其中 anonyomus 爲全局上下文棧;其他爲函數上下文棧函數

圖解: this

執行過程:es5

  1. 首先建立了全局執行上下文,壓入執行棧,其中的可執行代碼開始執行。
  2. 而後調用 changeColor 函數,JS引擎中止執行全局執行上下文,激活函數 changeColor 建立它本身的執行上下文,且把該函數上下文放入執行上下文棧頂,其中的可執行代碼開始執行。
  3. changeColor 調用了 swapColors 函數,此時暫停了 changeColor 的執行上下文,建立了 swapColors 函數的新執行上下文,且把該函數執行上下文放入執行上下文棧頂。
  4. swapColors 函數執行完後,其執行上下文從棧頂出棧,回到了 changeColor 執行上下文中繼續執行。
  5. changeColor 沒有可執行代碼,也沒有再遇到其餘執行上下文了,將其執行上下文從棧頂出棧,回到了 全局執行上下文 中繼續執行。
  6. 一旦全部代碼執行完畢,JS引擎將從當前棧中移除 全局執行上下文
注意:函數中,遇到return能直接終止可執行代碼的執行,所以會直接將當前上下文彈出棧。

使用 ECStack 來模擬調用棧:線程

ECStack=[]

JS第一次執行代碼時就會遇到全局代碼,執行上下文棧會壓入一個全局上下文,咱們用 globalContext 表示它,只有當整個應用程序結束的時候,ECStack 纔會被清空,因此 ECStack 最底部永遠有個 globalContext

ECStack.push(globalContext)

使用僞代碼模擬上述代碼行爲:

ECStack.push(<changeColor> functionContext);
ECStack.push(<swapColors> functionContext);

// swapColors出棧
ECStack.pop();
// changeColor出棧
ECStack.pop();

爲了鞏固一下執行上下文的理解,咱們再來繪製一個例子的演變過程,這是一個簡單的閉包例子。

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}
f1()() // 999

使用僞代碼模擬上述代碼行爲:

ECStack.push(<f1> functionContext);
// f1出棧
ECStack.pop();

ECStack.push(<f2> functionContext);
// f2出棧
ECStack.pop();

由於f1中的函數f2在f1的可執行代碼中,並無被調用執行,所以執行f1時,f2不會建立新的上下文,而直到f2執行時,才建立了一個新的。具體演變過程以下。

es3版本

es3版本執行上下文內有三個重要屬性:

  • 變量對象 VO(variable object)
  • 做用域鏈(scope chain)
  • this

能夠將每一個執行上下文抽象爲一個對象。

執行上下文的組成代碼示例:

executionContextObj = {
  scopeChain: { /* 變量對象(variableObject)+ 全部父執行上下文的變量對象*/ },
  [variableObject | activationObject]: {
    /*函數 arguments/參數,內部變量和函數聲明 */
    arguments,
    ...
  },
  this: {}
}

變量對象

變量對象 是與執行上下文相聯的數據做用域,用來存儲上下文中定義的變量和函數聲明。

不一樣執行上下文中的變量對象也不同:

  • 全局上下文 中的變量對象就是全局對象,在瀏覽器中就是 window 對象。在頂層 JavaScript 代碼中,能夠用關鍵字 this 引用全局對象。全部的全局變量和函數都是做爲 window 的屬性和方法存在。
console.log(this) //window
    var a=1 //掛到window上的屬性
    console.log(window.a) //1
    console.log(this.a) //1
  • 函數執行上下文 中咱們用活動對象 AO (activation object) 來表示變量對象,由於變量對象是規範上的或者說是引擎實現上的,在 JavaScript 環境中是不能被直接訪問的,只有當函數被調用時,變量對象被激活爲活動對象時,咱們才能訪問到其中的屬性和方法。
活動對象就是變量對象,只不過處於不一樣的狀態和階段而已。

做用域鏈

對於 JavaScript 來講做用域及做用域鏈的變量查詢是經過存儲在瀏覽器內存中的執行上下文實現的。當查找變量時,首先從當前上下文中的變量對象查找,若是沒有就會往上查找父級做用域中的變量對象,最終找到全局上下文的變量對象,若是沒有就報錯。這樣由多個執行上下文的變量對象構成的鏈表就叫作做用域鏈

那麼有同窗就有疑問了,做用域和執行上下文有什麼 區別 呢 :

函數執行上下文是在調用函數時, 函數體代碼執行以前建立,函數調用結束時就會自動釋放。由於不一樣的調用可能有不一樣的參數:

var a = 10;
function fn(x) {
  var a = 20;
  console.log(arguments)
  console.log(x)
}
fn(20)
fn(10) // 不一樣的調用可能有不一樣的參數

而JavaScript採用的是詞法做用域,fn 函數建立的做用域在函數定義時就已經肯定了;

**關聯 **:

做用域只是一個「地盤」,其中沒有變量,要經過做用域對應的執行上下文環境來獲取變量的值,因此做用域是靜態觀念的,而執行上下文環境是動態的。也就是說,做用域只是用於劃分你在這個做用域裏面定義的變量的有效範圍,出了這個做用域就無效。

同一個做用域下,對同一個函數的不一樣的調用會產生不一樣的執行上下文環境,繼而產生不一樣的變量的值,因此,做用域中變量的值是在執行過程當中肯定的,而做用域是在函數建立時就肯定的。

生命週期

執行上下文的生命週期有三個階段,分別是:

  • 建立階段
    • 生成變量對象
      • 建立arguments
      • 掃描函數聲明
      • 掃描變量聲明
    • 創建做用域鏈
    • 肯定this的指向
  • 執行階段
    • 變量賦值
    • 函數的引用
    • 執行其餘代碼
  • 銷燬階段

建立階段

**生成變量對象 **

  1. 建立arguments:若是是函數上下文,首先會建立 arguments 對象,給變量對象添加形參名稱和值。
  2. 掃描函數聲明:對於找到的函數聲明,將函數名和函數引用(指針)存入 VO 中,若是 VO 中已經有同名函數,那麼就進行覆蓋(重寫引用指針)。
  3. 掃描變量聲明:對於找到的每一個變量聲明,將變量名存入 VO 中,而且將變量的值初始化爲undefined 。若是變量的名字已經在變量對象裏存在,不會進行任何操做並繼續掃描。

讓咱們舉一個栗子來講明 :

function person(age) {
  console.log(typeof name); // function
  console.log(typeof getName); // undefined
  var name = 'abby';
  var hobby = 'game';
  var getName = function getName() {
    return 'Lucky';
  };
  function name() {
    return 'Abby';
  }
  function getAge() {
    return age;
  }
  console.log(typeof name); // string
  console.log(typeof getName); // function
  name = function () {};
  console.log(typeof name); // function
}
person(20);

在調用person(20)的時候,可是代碼還沒執行的時候,建立的狀態是這樣:

personContext = {
    scopeChain: { ... },
    activationObject: {
        arguments: {
            0: 20,
            length: 1
        },
        age: 20,
        name: pointer, // reference to function name(),
        getAge: pointer, // reference to function getAge(),
        hobby: undefined,
        getName : undefined,
    },
    this: { ... }
}

函數在執行以前,會先建立一個函數執行上下文,首先是指出函數的引用,而後按順序對變量進行定義,初始化爲 undefined存入到 VO 之中,在掃描到變量 name 時發如今 VO 之中存在同名的屬性(函數聲明變量),所以忽略。

全局執行上下文的建立沒有建立 arguments 這一步

創建做用域鏈

在執行期上下文的建立階段,做用域鏈是在變量對象以後建立的。做用域鏈自己包含變量對象。

  1. 當書寫一段函數代碼時,就會建立一個詞法做用域,這個做用域是函數內部的屬性,咱們用[[scope]]表示,它裏面保存父變量對象,因此[[scope]]就是一條層級鏈。
person.[[scope]] = [
     globalContext.variableObject
]
  1. 當函數調用,就意味着函數被激活了,此時建立函數上下文並壓入執行棧,而後複製函數 [[scope]] 屬性建立做用域鏈:
personContext = {
     scopeChain:person.[[scope]]
}
  1. 建立活動對象(前面的生成變量對象步驟),而後將活動對象(AO)推到做用域鏈的前端。
personContext = {
    activationObject: {
        arguments: {
            0: 20,
            length: 1
        },
        age: 20,
        name: pointer, // reference to function name(),
        getAge: pointer, // reference to function getAge(),
        hobby: undefined,
        getName : undefined,
    },
    scopeChain:[activationObject,[[scope]]]
}

肯定this的指向

若是當前函數被做爲對象方法調用或使用 bindcallapplyAPI 進行委託調用,則將當前代碼塊的調用者信息(this value)存入當前執行上下文,不然默認爲全局對象調用。

執行階段

執行階段 中,執行流進入函數而且在上下文中運行/解釋代碼,JS 引擎開始對定義的變量賦值、開始順着做用域鏈訪問變量、若是內部有函數調用就建立一個新的執行上下文壓入執行棧並把控制權交出

此時代碼從上到下執行的時候激活階段的過程是:

  1. 第一次執行 console.log; 此時 nameVO 中是函數。getName 未指定值在 VO 中的值是 undefined
  2. 執行到賦值代碼,getName 被賦值成函數表達式,name 被賦值爲 abby
  3. 第二次執行 console.log; 此時的 name 因爲函數被字符串賦值覆蓋所以是 string 類型getNamefunction 類型。
  4. 第三次執行 console.log; 此時的 name 因爲又被覆蓋所以是 function 類型

所以理解執行上下文以後很好解釋了變量提高(Hoisting):實際上變量和函數聲明在代碼裏的位置是不會改變的,而是在編譯階段被JavaScript引擎放入內存中

這就解釋了爲何咱們能在 name 聲明以前訪問它,爲何以後的 name 的類型值發生了變化,爲何 getName 第一次打印的時候是 undefined 等等問題了。

ES6 引入了 letconst 關鍵字,從而使 JavaScript 也能像其餘語言同樣擁有了塊級做用域,很好解決了變量提高帶來的一系列問題。

最後執行 console 時候的函數執行上下文:

personContext = {
    scopeChain: { ... },
    activationObject: {
        arguments: {
            0: 20,
            length: 1
        },
        age: 20,
        name: pointer, // reference to function name(),
        getAge: pointer, // reference to function getAge(),
        hobby: 'game',
        getName:pointer, pointer to function getName(),
    },
    this: { ... }
}

銷燬階段

通常來說當函數執行完成後,當前執行上下文(局部環境)會被彈出執行上下文棧而且等待虛擬機回收,控制權被從新交給執行棧上一層的執行上下文。

完整示例

示例一

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

一、執行全局代碼,生成全局上下文,而且壓入執行棧

ECStack=[
     globalContext
]
複製代碼

二、全局上下文初始化

globalContext={
     variableObject:[global,scope,checkscope],
     this:globalContext.variableObject,
     scopeChain:[globalContext.variableObject]
}

三、建立 checkscope 函數時生成內部屬性 [[scope]],並將全局上下文做用域鏈存入其中

checkscope.[[scope]] = [
     globalContext.variableObject
]

四、調用 checkscope 函數,建立函數上下文,壓棧

ECStack=[
     globalContext,
     checkscopeContext
]

五、此時 checkscope 函數還未執行,進入執行上下文

  • 複製函數 [[scope]] 屬性建立做用域鏈
  • 用 arguments 屬性建立活動對象
  • 初始化變量對象,加入變量聲明、函數聲明、形參
  • 活動對象壓入做用域鏈頂端
checkscopeContext = {
        activationObject: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: pointer, // reference to function f(),
        },
        scopeChain: [activationObject, globalContext.variableObject],
        this: undefined
    }

六、checkscope 函數執行,對變量 scope 設值

checkscopeContext = {
        activationObject: {
            arguments: {
                length: 0
            },
            scope: 'local scope',
            f: pointer, // reference to function f(),
        },
        scopeChain: [activationObject, globalContext.variableObject],
        this: undefined
    }

f 函數被建立生成 [[scope]] 屬性,並保存父做用域的做用域鏈

f.[[scope]]=[
     checkscopeContext.activationObject,
     globalContext.variableObject
]

七、f 函數調用,生成 f 函數上下文,壓棧

ECStack=[
     globalContext,
     checkscopeContext,
     fContext
]

八、此時 f 函數還未執行,初始化執行上下文

  • 複製函數 [[scope]] 屬性建立做用域鏈
  • 用 arguments 屬性建立活動對象
  • 初始化變量對象,加入變量聲明、函數聲明、形參
  • 活動對象壓入做用域鏈頂端
fContext = {
     activationObject: {
            arguments: {
                length: 0
            },
        },
        scopeChain: [fContext.activationObject, checkscopeContext.activationObject, globalContext.variableObject],
        this: undefined
    }

九、f 函數執行,沿着做用域鏈查找 scope 值,返回 scope 值

十、f 函數執行完畢,f函數上下文從執行上下文棧中彈出

ECStack=[
     globalContext,
     checkscopeContext
]

十一、checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出

ECStack=[
     globalContext
]

示例二

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
  1. 執行全局代碼,生成全局上下文,而且壓入執行棧
  2. 全局上下文初始化
  3. 建立 checkscope 函數時生成內部屬性 [[scope]],並將全局上下文做用域鏈存入其中
  4. 調用 checkscope 函數,建立函數上下文,壓棧
  5. 此時 checkscope 函數還未執行,進入執行上下文
    • 複製函數 [[scope]] 屬性建立做用域鏈
    • arguments 屬性建立活動對象
    • 初始化變量對象,加入變量聲明、函數聲明、形參
    • 活動對象壓入做用域鏈頂端
  6. checkscope 函數執行,對變量 scope 設值,f 函數被建立生成 [[scope]] 屬性,並保存父做用域的做用域鏈
  7. 返回函數f,此時 checkscope 函數執行完成,彈棧
  8. f 函數調用,生成 f 函數上下文,壓棧
  9. 此時 f 函數還未執行,初始化執行上下文
    • 複製函數 [[scope]] 屬性建立做用域鏈
    • arguments 屬性建立活動對象
    • 初始化變量對象,加入變量聲明、函數聲明、形參
    • 活動對象壓入做用域鏈頂端
  10. f 函數執行,沿着做用域鏈查找 scope 值,返回 scope
  11. f 函數執行完畢,f 函數上下文從執行上下文棧中彈出

能夠看到和前面惟一的區別就是 checkScope 函數執行完先出棧了,以後再執行 f 函數,步驟與示例一一致

fContext = {
    scopeChain: [activationObject, checkscopeContext.activationObject, globalContext.variableObject],
}

這裏在 checkscopeContext 函數執行完銷燬後,f 函數依然能夠讀取到 checkscopeContext.AO 的值,也就是說 checkscopeContext.AO 依然活在內存中,f 函數依然能夠經過 f 函數的做用域鏈找到它。而爲何 checkscopeContext.AO 沒有被銷燬,正是由於 f 函數引用了 checkscopeContext.AO 中的值,又正是由於JS實現了在子上下文引用父上下文的變量的時候,不會銷燬這些變量的效果實現了閉包 這個概念!

es5版本

ES5 規範去除了 ES3 中變量對象和活動對象,以 詞法環境組件( LexicalEnvironment component) 和 變量環境組件( VariableEnvironment component) 替代。

生命週期

es5 執行上下文的生命週期也包括三個階段:建立階段 → 執行階段 → 回收階段

建立階段

建立階段作了三件事:

  1. 肯定 this 的值,也被稱爲 This Binding

  2. LexicalEnvironment(詞法環境) 組件被建立

  3. VariableEnvironment(變量環境) 組件被建立

僞代碼大概以下:

ExecutionContext = {  
  ThisBinding = <this value>,     // 肯定this 
  LexicalEnvironment = { ... },   // 詞法環境
  VariableEnvironment = { ... },  // 變量環境
}
This Binding

ThisBinding 是和執行上下文綁定的,也就是說每一個執行上下文中都有一個 this,與 es3this 並無什麼區別,this 的值是在執行的時候才能確認,定義的時候不能確認

建立詞法環境

詞法環境的結構以下:

GlobalExectionContext = {  // 全局執行上下文
  LexicalEnvironment: {       // 詞法環境
    EnvironmentRecord: {     // 環境記錄
      Type: "Object",           // 全局環境
      // 標識符綁定在這裏 
      outer: <null>           // 對外部環境的引用
  }  
}

FunctionExectionContext = { // 函數執行上下文
  LexicalEnvironment: {     // 詞法環境
    EnvironmentRecord: {    // 環境記錄
      Type: "Declarative",      // 函數環境
      // 標識符綁定在這裏      // 對外部環境的引用
      outer: <Global or outer function environment reference>  
  }  
}

能夠看到詞法環境有兩種類型 :

  • 全局環境:是一個沒有外部環境的詞法環境,其外部環境引用爲 null。擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,this 的值指向這個全局對象。
  • 函數環境:用戶在函數中定義的變量被存儲在環境記錄中,包含了 arguments 對象。對外部環境的引用能夠是全局環境,也能夠是包含內部函數的外部函數環境。

詞法環境有兩個組件 :

  • 環境記錄器 :存儲變量和函數聲明的實際位置。
  • 外部環境的引用 :它指向做用域鏈的下一個對象,能夠訪問其父級詞法環境(做用域),做用與 es3 的做用域鏈類似

環境記錄器也有兩種類型 :

  • 在函數環境中使用 聲明式環境記錄器,用來存儲變量、函數和參數。
  • 在全局環境中使用 對象環境記錄器,用來定義出如今全局上下文中的變量和函數的關係。

所以:

  • 建立全局上下文的詞法環境使用 對象環境記錄器 ,outer 值爲 null;
  • 建立函數上下文的詞法環境時使用 聲明式環境記錄器 ,outer 值爲全局對象,或者爲父級詞法環境(做用域)
建立變量環境

變量環境也是一個詞法環境,所以它具備上面定義的詞法環境的全部屬性。

在 ES6 中,詞法環境和 變量環境的區別在於前者用於存儲函數聲明和變量( letconst關鍵字)綁定,然後者僅用於存儲變量( var )綁定,所以變量環境實現函數級做用域,經過詞法環境在函數做用域的基礎上實現塊級做用域。

🚨 使用 let / const 聲明的全局變量,會被綁定到 Script 對象而不是 Window 對象,不能以Window.xx 的形式使用;使用 var 聲明的全局變量會被綁定到 Window 對象;使用 var / let / const 聲明的局部變量都會被綁定到 Local 對象。注:Script 對象、Window 對象、Local 對象三者是平行並列關係。

箭頭函數沒有本身的上下文,沒有arguments,也不存在變量提高

使用例子進行介紹

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

遇到調用函數 multiply 時,函數執行上下文開始被建立:

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 標識符綁定在這裏  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 標識符綁定在這裏  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 標識符綁定在這裏  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 標識符綁定在這裏  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

變量提高的緣由:在建立階段,函數聲明存儲在環境中,而變量會被設置爲 undefined(在 var 的狀況下)或保持未初始化 uninitialized(在 let 和 const 的狀況下)。因此這就是爲何能夠在聲明以前訪問 var 定義的變量(儘管是 undefined ),但若是在聲明以前訪問 let 和 const 定義的變量就會提示引用錯誤的緣由。這就是所謂的變量提高。

圖解變量提高:

var myname = "極客時間"
function showName(){
  console.log(myname);
  if(0){
   var myname = "極客邦"
  }
  console.log(myname);
}
showName()

在 showName 內部查找 myname 時會先使用當前函數執行上下文裏面的變量 myname ,因爲變量提高,當前的執行上下文中就包含了變量 myname,而值是 undefined,因此獲取到的 myname 的值就是 undefined。

執行階段

在此階段,完成對全部這些變量的分配,最後執行代碼,若是 JavaScript 引擎不能在源碼中聲明的實際位置找到 let 變量的值,它會被賦值爲 undefined

回收階段

執行上下文出棧等待虛擬機回收執行上下文

過程總結

  1. 建立階段 首先建立全局上下文的詞法環境:首先建立 對象環境記錄器,接着建立他的外部環境引用 outer,值爲 null
  2. 建立全局上下文的語法環境:過程同上
  3. 肯定 this 值爲全局對象(以瀏覽器爲例,就是 window )
  4. 函數被調用,建立函數上下文的詞法環境:首先建立 聲明式環境記錄器,接着建立他的外部環境引用 outer,值爲 null,值爲全局對象,或者爲父級詞法環境
  5. 建立函數上下文的變量環境:過程同上
  6. 肯定 this 值
  7. 進入函數執行上下文的 執行階段
  8. 執行完成後進入 回收階段

實例講解

將詞法環境中 outer 抽離出來,執行上下文結構以下:

下面咱們以以下示例來分析執行上下文的建立及執行過程:

function foo(){
  var a = 1
  let b = 2
  {
    let b = 3
    var c = 4
    let d = 5
    console.log(a)
    console.log(b)
  }
  console.log(b) 
  console.log(c)
  console.log(d)
}   
foo()

第一步: 調用 foo 函數前先編譯並建立執行上下文,在編譯階段將 var 聲明的變量存放到變量環境中,let 聲明的變量存放到詞法環境中,須要注意的是此時在函數體內部塊做用域中 let 聲明的變量不會被存放到詞法環境中,以下圖所示 :

第二步: 繼續執行代碼,當執行到代碼塊裏面時,變量環境中的 a 的值已經被設置爲1,詞法環境中 b 的值已經被設置成了2,此時函數的執行上下文如圖所示:

從圖中就能夠看出,當進入函數的做用域塊時,做用域塊中經過 let 聲明的變量,會被存放在詞法環境的一個單獨的區域中,這個區域中的變量並不影響做用域塊外面的變量,所以示例中在函數體內塊做用域中聲明的變量的 b 與函數做用域中聲明的變量 b 都是獨立的存在。

在詞法環境內部,實際上維護了一個小型棧結構,棧底是函數最外層的變量,進入一個做用域塊後,就會把該做用域內部的變量壓到棧頂;當該塊級做用域執行完成以後,該做用域的信息就會從棧頂彈出,這就是詞法環境的結構。

第三步: 當代碼執行到做用域塊中的 console.log(a) 時,就須要在詞法環境和變量環境中查找變量 a 的值了,具體查找方式是:沿着詞法環境的棧頂向下查詢,若是在詞法環境中的某個塊中查找到了,就直接返回給 JavaScript 引擎,若是沒有查找到,那麼繼續在變量環境中查找。

這樣一個變量查找過程就完成了,你能夠參考下圖:

第四步: 當函數體內塊做用域執行結束以後,其內部變量就會從詞法環境的棧頂彈出,此時執行上下文以下圖所示:

第五步: 當foo函數執行完畢後執行棧將foo函數的執行上下文彈出。

因此,塊級做用域就是經過詞法環境的棧結構來實現的,而變量提高是經過變量環境來實現,經過這二者的結合,JavaScript 引擎也就同時支持了變量提高和塊級做用域了。

outer引用

outer 是一個外部引用,用來指向外部的執行上下文,其是由詞法做用域指定的

function bar() {
  console.log(myName)
}
function foo() {
  var myName = " 極客邦 "
  bar()
}
var myName = " 極客時間 "
foo()

當一段代碼使用了一個變量時,JavaScript 引擎首先會在「當前的執行上下文」中查找該變量, 好比上面那段代碼在查找 myName 變量時,若是在當前的變量環境中沒有查找到,那麼 JavaScript 引擎會繼續在 outer 所指向的執行上下文中查找。爲了直觀理解,你能夠看下面這張圖:

從圖中能夠看出,bar 函數和 foo 函數的 outer 都是指向全局上下文的,這也就意味着若是在 bar 函數或者 foo 函數中使用了外部變量,那麼 JavaScript 引擎會去全局執行上下文中查找。咱們把這個查找的鏈條就稱爲做用域鏈。 如今你知道變量是經過做用域鏈來查找的了,不過還有一個疑問沒有解開,foo 函數調用的 bar 函數,那爲何 bar 函數的外部引用是全局執行上下文,而不是 foo 函數的執行上下文?

這是由於在 JavaScript 執行過程當中,其做用域鏈是由詞法做用域決定的。詞法做用域指做用域是由代碼中函數聲明的位置來決定的,所以是靜態的做用域

結合變量環境、詞法環境以及做用域鏈,咱們看下下面的代碼:

function bar() {
  var myName = " 極客世界 "
  let test1 = 100
  if (1) {
    let myName = "Chrome 瀏覽器 "
    console.log(test)
  }
}
function foo() {
  var myName = " 極客邦 "
  let test = 2
  {
    let test = 3
    bar()
  }
}
var myName = " 極客時間 "
let myAge = 10
let test = 1
foo()

對於上面這段代碼,當執行到 bar 函數內部的 if 語句塊時,其調用棧的狀況以下圖所示:

解釋下這個過程。首先是在 bar 函數的執行上下文中查找,但由於 bar 函數的執行上下文中沒有定義 test 變量,因此根據詞法做用域的規則,下一步就在 bar 函數的外部做用域中查找,也就是全局做用域。

相關文章
相關標籤/搜索