javascript執行上下文詳解
簡介
執行上下文可以說是js代碼執行的一個環境,存放瞭代碼執行所需的變量,變量查找的作用域鏈規則以及this指向等。同時,它也是js很底層的東西,很多的問題如變量提升、作用域鏈和閉包等都可以在執行上下文中找到答案,所以這也是我們學習執行上下文的原因
執行上下文分為三種:
- 全局執行上下文:當進入全局代碼時會進行編譯,在編譯中創建全局執行上下文,並生成可執行代碼
- 函數執行上下文:執行代碼的過程中,如果遇到函數調用,會編譯函數內的代碼和創建函數執行上下文,並創建可執行代碼
- eval執行上下文:當使用eval函數的時候,eval的代碼也會被編譯,並創建執行上下文
因為執行上下文是在編譯階段創建的,所以接下來先看一下js代碼的執行過程吧
javascript代碼的執行過程
一段js代碼的執行過程中,先是會進行編譯階段,js引擎會將代碼進行編譯,再進入執行階段
也就是說,js代碼是按照“段”來執行的,具體就是全局代碼就是一段代碼,函數執行也算一段代碼,編譯也是按照“段”來編譯的,也就是一整個js代碼會出現多個編譯階段
編譯階段
編譯階段是一個很復雜的過程,這裡隻是簡單的介紹:
1、編譯階段完成兩件事情:創建執行上下文和生成可執行代碼
2、執行上下文就包括變量環境和詞法環境和this指向等,創建執行上下文的過程:
- 如果是普通變量的話,js引擎會將該變量添加到變量環境中並初始化為undefined
- 如果是函數聲明的話,js引擎會將函數定義添加到變量環境中,然後將函數名執行該函數的位置(內存)
3、接著,js引擎就會把其他的代碼編譯為字節碼,生成可執行代碼
編譯階段完成後,js引擎開始執行可執行代碼,按照順序一行一行執行,當遇到函數或者變量時,會在變量環境中尋找,找不到的話就會報錯
如果遇到賦值語句時,就會將值賦值給變量
var變量提升與let和const
變量提升是指在js代碼執行過程中,js引擎把變量的聲明部分和函數聲明部分提升到代碼開頭的“行為”。變量被提升後,會給變量設置默認值undefined
變量提升的實現並不是物理地移動代碼的位置,而是在編譯階段被js引擎放入內存中。
1、普通變量提升會賦值為undefined,函數變量名會將整個函數提升
console.log(fn); // [Function: fn] console.log(a); // undefined function fn() { console.log(111); } var a = 1
2、函數表達式隻會將變量提升,不會將函數題提升
3、當有多個相同類型的聲明(同樣是函數聲明或者同樣是普通變量聲明),最後一個聲明會覆蓋之前的聲明
4、當函數聲明與變量聲明同時出現時,函數聲明優先級更高
當然,變量提升有很多的缺陷,所以從es6開始引入瞭let和const關鍵字,通過let和const聲明的變量不具有變量提升特性,同時也支持塊級作用域,先看一下作用域吧
作用域
作用域其實就是一套定義函數調用和變量使用的規則,其中,就有三種作用域:
- 全局作用域:其中對象在代碼的任何地方都能訪問,其生命周期伴隨著頁面的生命周期
- 函數作用域:在函數內部定義的變量和函數,隻能在內部訪問,外部不能訪問到,函數執行結束後,函數內部定義的變量會被銷毀(函數不會嗎???)
- 塊級作用域:由代碼塊包含的代碼會形成一個塊級作用域(es6之前沒有),跟函數作用域類似
通過var聲明的變量沒有塊級作用域,通過const let聲明的變量有塊級作用域
那js是如何var的變量提升和支持塊級作用域的呢?這就得從執行上下文的角度說起
編譯階段生成執行上下文:
假設js需要執行一個函數
- 首先,編譯創建該函數的執行上下文,創建可執行代碼
- 在編譯階段,所有通過var聲明的變量(包括代碼塊裡面的變量)都會被創建並存放在變量環境中,並初始化為undefined
- 通過let或者const聲明的變量(不包括代碼塊碼裡面的變量)都會被創建並存放在詞法環境中,設置為未初始化
- 至此,編譯階段結束瞭,開始執行代碼
- 執行代碼過程中遇到代碼塊時,會先將裡面通過let或者const聲明的變量存放在詞法環境中並設置為初始化,其實,在詞法環境內部,維護瞭一個小型的棧結構,棧底是函數最外層的變量,每遇到一個代碼塊,就將所包含的變量壓入詞法環境的棧結構,代碼塊執行結束後,就將包含的變量彈出
接下來看一段代碼:
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函數執行上 | |
---|---|
變量環境 a = 1, c = 3 |
詞法環境 {b = 3, d = 5} {b = 2} |
當代碼塊的代碼執行完畢後,對應的詞法環境裡的變量就會被彈出棧
foo函數執行上下文 | |
---|---|
變量環境 a = 1, c = 3 |
詞法環境 {b = 2} |
原因
通過上面的分析,我們可以總結變量提升和塊級作用域的實現:
- 通過編譯階段,通過var聲明的變量已經存在變量環境中並賦值為undefined,所以在執行代碼的任何位置都能狗訪問得到,而不需要在聲明之後才能訪問
- 而通過let聲明的變量,會被存放在詞法環境中但並未初始化(不包含代碼塊的let或const聲明的變量),所以並不能訪問,而是等到遇到let聲明語句的時候才初始化並賦值
- 在遇到代碼塊時,會先將let和const聲明的變量存放在詞法環境中並設置為初始化,如果此時在代碼塊中在let聲明變量之前使用該變量,並不會去外部作用域找該變量,因為此時詞法作用域已經存在改變量瞭,但未初始化,所以此時會報錯誤,這也是let暫時性死區的原因
單個執行上下文中變量的查找規則
沿著詞法環境的棧頂向下查詢,如果在詞法環境的某個快中查找到瞭,就直接返回給js引擎,如果沒有找到,就繼續在變量環境中查找
調用棧
調用棧是用來管理函數調用關系的一種棧結構
在函數調用之前,會創建對應的執行上下文,並生成對應的可執行代碼
- js維護瞭一個棧結構,每當遇到一個函數調用的時候,就創建一個執行上下文,並壓入該棧中,
- 這個棧叫做執行上下文棧,也叫做調用棧
- 當函數執行完畢之後,會將對應的執行上下文彈出棧結構
- 棧的容量是有限的,當棧容量不夠的時候就有可能發生棧溢出
作用域鏈
- 在函數中如果在當前作用域中找不到所需要的變量,就得沿著作用域鏈往下去查找,直到找到為止
- 我們都知道,當一段代碼在執行的時候,會有對應的執行上下文,那變量沿著作用域鏈查看的規則也是在執行上下文中設置的
- 在每個執行上下文中,在變量環境中,都有一個外部引用,用來執行外部的執行上下文,我們把這個外部引用稱為outer
- 上文已經說到,變量的查找首先會從執行上下文的詞法環境中查找,找不到就在變量環境中查找,再找不到的話就會沿著outer去外部的執行上下文中查找
- outer具體引用哪一個執行上下文(作用域),是由詞法作用域決定的
詞法作用域
詞法作用域指的是作用域有代碼中函數的聲明位置決定的,也叫做靜態作用域
也就是說,當創建一個執行上下文的時候,其內部的outer就會根據詞法作用域去執行對應的外部執行上下文
在外部的執行上下文中查找時,也是先從詞法環境中開始
function fn() { console.log(a); } function fn1() { let a = 1 fn() } let a = 3 // let聲明的變量是在詞法環境中的 fn1() // 3
閉包
在js中,根據詞法作用域的規則,內部函數總是可以訪問外部函數中聲明的變量,當通過調用一個外部函數返回一個內部函數後,即使該外部函數已經執行結束瞭,但是內部函數引用外部函數的變量依然保存在內存中,我們就把這些變量的集合稱為閉包
function foo() { var myName = " 極客時間 " let test1 = 1 const test2 = 2 var innerBar = { getName:function(){ console.log(test1) return myName }, setName:function(newName){ myName = newName } } return innerBar } var bar = foo() bar.setName(" 極客邦 ") bar.getName() console.log(bar.getName())
上面代碼中,由於存在閉包現象,foo函數執行結束後,內部的變量還會被保存,調用棧如下圖:
當執行到 bar.setName 方法中的myName = "極客邦"這句代碼時,JavaScript 引擎會沿著“當前執行上下文–>foo 函數閉包–> 全局執行上下文”的順序來查找 myName 變量,你可以參考下面的調用棧狀態圖:
閉包的回收
- 如果閉包使用不正確,會很容易造成內存泄漏的,關註閉包是如何回收的能讓你正確地使用閉包。
- 通常,如果引用閉包的函數是一個全局變量,那麼閉包會一直存在直到頁面關閉;但如果這個閉包以後不再使用的話,就會造成內存泄漏。
- 如果引用閉包的函數是個局部變量,等函數銷毀後,在下次 JavaScript 引擎執行垃圾回收時,判斷閉包這塊內容如果已經不再被使用瞭,那麼 JavaScript 引擎的垃圾回收器就會回收這塊內存。
- 所以在使用閉包的時候,你要盡量註意一個原則:如果該閉包會一直使用,那麼它可以作為全局變量而存在;但如果使用頻率不高,而且占用內存又比較大的話,那就盡量讓它成為一個局部變量。
var bar = { myName:"time.geekbang.com", printName: function () { console.log(myName) } } function foo() { let myName = " 極客時間 " return bar.printName } let myName = " 極客邦 " let _printName = foo() _printName() bar.printName()
從上下文角度講this
執行上下文分為三種,對應的this也隻有三種:全局上下文的this,函數中的this,eval中的this
- 箭頭函數沒有自己的執行上下文
- 全局上下文的this指向全局對象
- 函數上下文的this根據四種綁定規則判斷this指向
- 執行上下文包含this指向
參考:https://time.geekbang.org/column/intro/100033601
推薦閱讀:
- JS作用域作用鏈及this使用原理詳解
- JavaScript三大變量聲明詳析
- 簡單談談JavaScript變量提升
- JavaScript展開運算符和剩餘運算符的區別詳解
- Javascript基礎學習之十個重要問題