關於js中的執行上下文

本文原創:dongdezhangjavascript

寫在前面

你們都據說過execution context(執行上下文或執行環境),在操做系統中也有相似的上下文概念,它指得是存儲在各寄存器中的中間數據,還有context switchs(上下文切換)等概念,那js中的context跟os中的context相比,是否是也有殊途同歸之妙吶?前端

看個題目,試想下瀏覽器是如何執行這段代碼?java

let a = 1
var b = 2
function jad() {
    var a = 2
    jdzw()
}
function jdzw() {
    console.log(a, b)
}
jad() //1 2
複製代碼

執行上下文

相關的幾個關鍵詞

  • 執行棧(Execution Stack/Calling Stack)
  • 變量提高(Hoisting)
  • 做用域鏈(Scope Chain)
  • 閉包(Closure)
  • 詞法環境(LexicalEnvironment)
  • 變量環境(VariableEnvironment)

首先要明確的是執行上下文跟做用域鏈是兩個不一樣的概念node

定義:執行上下文是一個"環境"的抽象概念,在這個「環境」中Javascript代碼被分析和執行。任何代碼在JavaScript中運行時,都是在執行上下文中運行的。chrome

上下文的類型

  1. 全局執行上下文(global execution context) 這就是咱們一般所說的全局環境,這裏所能訪問的變量都掛載到一個global object上,在瀏覽器中就是window對象,在node中就是 global 對象。程序在執行時只會有一個全局上下文存在
  2. 函數執行上下文(function execution context) 每個函數在調用執行時都會建立一個上下文,當函數上下文被建立時,按照執行順序會被推到執行棧中(具體過程稍後會講),這也就意味這程序執行過程當中會有多個函數上下文,而且還伴隨着上下文的銷燬。
  3. eval函數執行上下文 代碼在eval函數執行時也會有本身的上下文,開發過程當中並不常涉及到,僅做爲一個類型瞭解。

執行棧

簡單理解就是遵循 LIFO 的棧結構,結合圖解和動圖感覺一下上述函數的執行過程,目前咱們只須要關注圖中call stack 和 scope下的內容便可,圖片是在chrome調試下截取,關於chrome調試工具使用能夠參考瀏覽器

1.jpg

具體的執行過程:緩存

  1. 當瀏覽器加載完成代碼後,首先會建立global execution context推到棧中,
  2. 當執行jad()時,會建立該函數的執行上下文,推到stack頂部;
  3. 當遇到jdzw()時一樣也是新建函數上下文,推到stack頂。
  4. 每個函數執行完後,會依次pop出本身的上下文,最終只會保留global 上下文。
  5. 當程序執行完,瀏覽器頁面關閉,全局上下文出棧並銷燬。

34.gif

上下文的生命週期

上圖中咱們發現它 scope 中存在 local 和 global 這兩個變量,local 中保存有當前函數能夠訪問到的變量名,而 global 其實就是 window,保存全局的一些變量,這其實就是一個簡單的做用域鏈。閉包

上下文的生命週期分兩個階段:建立階段和執行階段app

建立階段

在這期間會建立 LexicalEnvironment 和 VariableEnvironment 兩部分,用僞代碼表示以下:ide

ExecutionContext = {
  LexicalEnvironment: {},
  VariableEnvironment: {},
}
複製代碼

LexicalEnvironment是什麼?

簡單理解爲是**標識符-變量的映射(identifier-variable mapping)**標識符指的是函數或變量的名稱,變量指得是真正的引用對象或基本類型數據。它包含三個結構:

  1. environment record(環境記錄)
  2. outer environment(外部的環境)
  3. this(this的綁定)

environment record

對於 Environment Record,又分爲 Declarative environment record(聲明式環境記錄)和Object environment record(對象式環境記錄)兩種,

聲明式環境記錄主要用在函數上下文中,包含變量、函數和 arguments 對象;相反對象式環境記錄則用於全局上下文中,主要用於記錄全局的對象,函數和變量。

outer environment

outer environment 指向外部的 LexicalEnvironment,這樣能夠獲取到外部的數據。全局上下文的outer老是指向null。

this

this綁定依賴於函數的執行方式,全局環境中this指向global object。關於this的理解推薦查看以前的文章

VariableEnvironment又是什麼?

其實它也是一種 LexicalEnvironment,只不過是用來保存用 var 聲明的變量,與let、const聲明的變量區分開。

舉個例子🌰

let a = 1
var b = 2
var h
function jad() {
    var c = 2
    let d = 3
    return c + d
}
h = jad(12)
複製代碼

上述代碼的全局上線文建立過程以下:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object", // 對象式環境記錄用於全局上下文中
      a: <uninitialized>, // let聲明在建立時期uninitialized
      jad: <function>
    }
    outer: <null>,
    this: <global object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      b: undefined, // var 聲明undefined
      h: undefined
    }
    outer: <null>,
    this: <global object>
  }
}
複製代碼

jad 函數執行時,函數上下文的建立過程以下

JadFunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative", // 函數上下文的環境記錄爲Declarative類型
      d: <uninitialized>,
      Arguments: {0: 1, 1: 2, length: 2// 函數上下文包含Arguments對象
    }
    outer: <GlobalExectionContext>, // 指向上層上下文
    this: <global>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      c: undefined,
    }
    outer: <GlobalExectionContext>,
    this: <global>
  }
}
複製代碼

能夠觀察到 let 聲明的變量在建立過程當中是未初始化的,而 var 聲明的變量是 undefined,這也就是爲何在 var 以前訪問變量會輸出 undefined,而 let 會報錯。這其實就是 var 變量提高 和 let 的TDZ(臨時性死區),而且函數的變量提高優先級高,所以會出現變量覆蓋,另外函數表達式並不會提高

但若是在 let 聲明以後訪問,雖然沒賦值,引擎也會默認undefined

var a = 1
function a() {

}
var b = 2
var b = function(){}
if(true) {
    console.log(c) // ReferenceError: c is not defined
    let c
}
console.log(a, b) // 1 function
複製代碼

執行階段

當進入執行階段,上述代碼的全局上下文會更新以下:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      a: 1, // 執行完 a = 1
      jad: <function>
    }
    outer: <null>,
    this: <global object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      b: 2, // 執行完 b =2
      h: undefined
    }
    outer: <null>,
    this: <global object>
  }
}
複製代碼

jad執行時,函數上下文的更新以下

JadFunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative", // 函數上下文的環境記錄Declarative
      d: 3, // 執行let d = 3
      Arguments: {0: 1, 1: 2, length: 2
    }
    outer: <GlobalExectionContext>,
    this: <global>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      c: 2, // 執行 var c = 2
    }
    outer: <GlobalExectionContext>,
    this: <global>
  }
}
複製代碼

jad 函數執行完後,將返回值更新到變量 h

在ES6以前,上下文能夠簡單理解爲下面這種結構:

  • vo:指的是變量對象(variable object)
  • ao:指的是活動對象(activation object)
  • 在建立過程到執行過程vo => ao,變量被激活賦值,同時還伴隨着this的綁定及做用域鏈的建立
// 上下文在建立階段
Context = {
    vo: {
      a: undefined  // 各類變量 undefined
    },
    outer:<other context or null>
    this:<global or undefined>
}
// 上下文在執行階段
Context = {
    ao: {
      a: 1  // 變量賦值
    },
    outer:<other context or null>
    this:<global or undefined>
}
複製代碼

做用域鏈

做用域簡單分爲全局做用域,函數做用域和塊級做用域

let a = 1
var b = 2 // 全局做用域
function jad() {
    var a = 2 // 函數做用域
    jdzw()
    if(true) { // 塊級做用域
        let f = 3
        let a = 4
        console.log(f, a)// 3 4
    }
}
function jdzw() {
    console.log(a, b)// 1 2
}
jad() 
複製代碼

78.gif

在上圖中的scope看到的local global block等屬性,這其實就是當前函數的做用域鏈。local其實能夠理解爲函數做用域。本質上是在執行上下文建立的過程當中,outer指向的外部環境,這樣就構成了相似鏈表式的結構,做用域鏈的終點始終是global object,在瀏覽器中也就是window對象。

做用域鏈保證了函數對執行環境有權訪問的全部變量和函數的有序訪問

延長做用域

js中的某些語句能夠延長做用域鏈

  • try-catch的catch塊
  • with語句
function url() {
    var s = '?debug=true'
    with(location) {
        console.log(s + href)// 此處能夠訪問到href
    }
}
try {
    throw new Error('error')
} catch(err) { // catch並非一個函數,執行到此處時 err被放到當前做用域鏈的最前端 
    console.log(err) // error
}
複製代碼

56.gif

閉包

閉包指的是有權訪問另外一個函數做用域中的變量的函數。

閉包爲何能達到這樣的效果?

究其緣由是由於閉包函數定義在父級函數的內部,所以閉包函數的執行上下文在建立的過程當中,天然將outer指向父級函數的執行上下文。

以下 c 函數就是一個閉包,固然建立的過程也可以使用匿名函數 閉包這一特色能夠用來緩存數據。好比函數柯理化等,也讓函數執行過程變得複雜多變,這也是js做爲弱語言吸引人的特色之一。❤️

let a = 1
var b = 2
function jad() {
    var a = 2
    function jdzw() {
        console.log(a, b) //2 2 可以訪問到jad中的a
    }
    return jdzw
}

let c = jad()
c() //2 2 可以訪問到jad中的a
複製代碼

最後

但願讀完以後,可以對js的執行上下文,做用域,閉包及變量提高有所收穫。(我的感受跟os中的context的概念大同小異)

let a = 'hello'
(function (callback) {
  let b = 'world'
  callback()
})(function () {
    console.log(a)
    console.log(b)
  })
// hello world or hello error?
複製代碼

參考文章

相關文章
相關標籤/搜索