本文原創: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
複製代碼
首先要明確的是執行上下文跟做用域鏈是兩個不一樣的概念node
定義:執行上下文是一個"環境"的抽象概念,在這個「環境」中Javascript代碼被分析和執行。任何代碼在JavaScript中運行時,都是在執行上下文中運行的。chrome
簡單理解就是遵循 LIFO
的棧結構,結合圖解和動圖感覺一下上述函數的執行過程,目前咱們只須要關注圖中call stack 和 scope下的內容便可,圖片是在chrome調試下截取,關於chrome調試工具使用能夠參考瀏覽器
具體的執行過程:緩存
上圖中咱們發現它 scope 中存在 local 和 global 這兩個變量,local 中保存有當前函數能夠訪問到的變量名,而 global 其實就是 window,保存全局的一些變量,這其實就是一個簡單的做用域鏈。閉包
上下文的生命週期分兩個階段:建立階段和執行階段app
在這期間會建立 LexicalEnvironment 和 VariableEnvironment 兩部分,用僞代碼表示以下:ide
ExecutionContext = {
LexicalEnvironment: {},
VariableEnvironment: {},
}
複製代碼
簡單理解爲是**標識符-變量的映射(identifier-variable mapping)**標識符指的是函數或變量的名稱,變量指得是真正的引用對象或基本類型數據。它包含三個結構:
對於 Environment Record,又分爲 Declarative environment record(聲明式環境記錄)和Object environment record(對象式環境記錄)兩種,
聲明式環境記錄主要用在函數上下文中,包含變量、函數和 arguments 對象;相反對象式環境記錄則用於全局上下文中,主要用於記錄全局的對象,函數和變量。
outer environment 指向外部的 LexicalEnvironment,這樣能夠獲取到外部的數據。全局上下文的outer老是指向null。
this綁定依賴於函數的執行方式,全局環境中this指向global object。關於this的理解推薦查看以前的文章
其實它也是一種 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(1,2)
複製代碼
上述代碼的全局上線文建立過程以下:
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以前,上下文能夠簡單理解爲下面這種結構:
// 上下文在建立階段
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()
複製代碼
在上圖中的scope看到的local global block等屬性,這其實就是當前函數的做用域鏈。local其實能夠理解爲函數做用域。本質上是在執行上下文建立的過程當中,outer指向的外部環境,這樣就構成了相似鏈表式的結構,做用域鏈的終點始終是global object,在瀏覽器中也就是window對象。
做用域鏈保證了函數對執行環境有權訪問的全部變量和函數的有序訪問
js中的某些語句能夠延長做用域鏈
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
}
複製代碼
閉包指的是有權訪問另外一個函數做用域中的變量的函數。
閉包爲何能達到這樣的效果?
究其緣由是由於閉包函數定義在父級函數的內部,所以閉包函數的執行上下文在建立的過程當中,天然將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?
複製代碼