javascript執(zhí)行上下文詳解
簡(jiǎn)介
執(zhí)行上下文可以說是js代碼執(zhí)行的一個(gè)環(huán)境,存放了代碼執(zhí)行所需的變量,變量查找的作用域鏈規(guī)則以及this指向等。同時(shí),它也是js很底層的東西,很多的問題如變量提升、作用域鏈和閉包等都可以在執(zhí)行上下文中找到答案,所以這也是我們學(xué)習(xí)執(zhí)行上下文的原因
執(zhí)行上下文分為三種:
- 全局執(zhí)行上下文:當(dāng)進(jìn)入全局代碼時(shí)會(huì)進(jìn)行編譯,在編譯中創(chuàng)建全局執(zhí)行上下文,并生成可執(zhí)行代碼
- 函數(shù)執(zhí)行上下文:執(zhí)行代碼的過程中,如果遇到函數(shù)調(diào)用,會(huì)編譯函數(shù)內(nèi)的代碼和創(chuàng)建函數(shù)執(zhí)行上下文,并創(chuàng)建可執(zhí)行代碼
- eval執(zhí)行上下文:當(dāng)使用eval函數(shù)的時(shí)候,eval的代碼也會(huì)被編譯,并創(chuàng)建執(zhí)行上下文
因?yàn)閳?zhí)行上下文是在編譯階段創(chuàng)建的,所以接下來先看一下js代碼的執(zhí)行過程吧
javascript代碼的執(zhí)行過程
一段js代碼的執(zhí)行過程中,先是會(huì)進(jìn)行編譯階段,js引擎會(huì)將代碼進(jìn)行編譯,再進(jìn)入執(zhí)行階段
也就是說,js代碼是按照“段”來執(zhí)行的,具體就是全局代碼就是一段代碼,函數(shù)執(zhí)行也算一段代碼,編譯也是按照“段”來編譯的,也就是一整個(gè)js代碼會(huì)出現(xiàn)多個(gè)編譯階段
編譯階段
編譯階段是一個(gè)很復(fù)雜的過程,這里只是簡(jiǎn)單的介紹:
1、編譯階段完成兩件事情:創(chuàng)建執(zhí)行上下文和生成可執(zhí)行代碼
2、執(zhí)行上下文就包括變量環(huán)境和詞法環(huán)境和this指向等,創(chuàng)建執(zhí)行上下文的過程:
- 如果是普通變量的話,js引擎會(huì)將該變量添加到變量環(huán)境中并初始化為undefined
- 如果是函數(shù)聲明的話,js引擎會(huì)將函數(shù)定義添加到變量環(huán)境中,然后將函數(shù)名執(zhí)行該函數(shù)的位置(內(nèi)存)
3、接著,js引擎就會(huì)把其他的代碼編譯為字節(jié)碼,生成可執(zhí)行代碼
編譯階段完成后,js引擎開始執(zhí)行可執(zhí)行代碼,按照順序一行一行執(zhí)行,當(dāng)遇到函數(shù)或者變量時(shí),會(huì)在變量環(huán)境中尋找,找不到的話就會(huì)報(bào)錯(cuò)
如果遇到賦值語句時(shí),就會(huì)將值賦值給變量
var變量提升與let和const
變量提升是指在js代碼執(zhí)行過程中,js引擎把變量的聲明部分和函數(shù)聲明部分提升到代碼開頭的“行為”。變量被提升后,會(huì)給變量設(shè)置默認(rèn)值undefined
變量提升的實(shí)現(xiàn)并不是物理地移動(dòng)代碼的位置,而是在編譯階段被js引擎放入內(nèi)存中。
1、普通變量提升會(huì)賦值為undefined,函數(shù)變量名會(huì)將整個(gè)函數(shù)提升
console.log(fn); // [Function: fn] console.log(a); // undefined function fn() { console.log(111); } var a = 1
2、函數(shù)表達(dá)式只會(huì)將變量提升,不會(huì)將函數(shù)題提升
3、當(dāng)有多個(gè)相同類型的聲明(同樣是函數(shù)聲明或者同樣是普通變量聲明),最后一個(gè)聲明會(huì)覆蓋之前的聲明
4、當(dāng)函數(shù)聲明與變量聲明同時(shí)出現(xiàn)時(shí),函數(shù)聲明優(yōu)先級(jí)更高
當(dāng)然,變量提升有很多的缺陷,所以從es6開始引入了let和const關(guān)鍵字,通過let和const聲明的變量不具有變量提升特性,同時(shí)也支持塊級(jí)作用域,先看一下作用域吧
作用域
作用域其實(shí)就是一套定義函數(shù)調(diào)用和變量使用的規(guī)則,其中,就有三種作用域:
- 全局作用域:其中對(duì)象在代碼的任何地方都能訪問,其生命周期伴隨著頁面的生命周期
- 函數(shù)作用域:在函數(shù)內(nèi)部定義的變量和函數(shù),只能在內(nèi)部訪問,外部不能訪問到,函數(shù)執(zhí)行結(jié)束后,函數(shù)內(nèi)部定義的變量會(huì)被銷毀(函數(shù)不會(huì)嗎???)
- 塊級(jí)作用域:由代碼塊包含的代碼會(huì)形成一個(gè)塊級(jí)作用域(es6之前沒有),跟函數(shù)作用域類似
通過var聲明的變量沒有塊級(jí)作用域,通過const let聲明的變量有塊級(jí)作用域
那js是如何var的變量提升和支持塊級(jí)作用域的呢?這就得從執(zhí)行上下文的角度說起
編譯階段生成執(zhí)行上下文:
假設(shè)js需要執(zhí)行一個(gè)函數(shù)
- 首先,編譯創(chuàng)建該函數(shù)的執(zhí)行上下文,創(chuàng)建可執(zhí)行代碼
- 在編譯階段,所有通過var聲明的變量(包括代碼塊里面的變量)都會(huì)被創(chuàng)建并存放在變量環(huán)境中,并初始化為undefined
- 通過let或者const聲明的變量(不包括代碼塊碼里面的變量)都會(huì)被創(chuàng)建并存放在詞法環(huán)境中,設(shè)置為未初始化
- 至此,編譯階段結(jié)束了,開始執(zhí)行代碼
- 執(zhí)行代碼過程中遇到代碼塊時(shí),會(huì)先將里面通過let或者const聲明的變量存放在詞法環(huán)境中并設(shè)置為初始化,其實(shí),在詞法環(huán)境內(nèi)部,維護(hù)了一個(gè)小型的棧結(jié)構(gòu),棧底是函數(shù)最外層的變量,每遇到一個(gè)代碼塊,就將所包含的變量壓入詞法環(huán)境的棧結(jié)構(gòu),代碼塊執(zhí)行結(jié)束后,就將包含的變量彈出
接下來看一段代碼:
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()
當(dāng)執(zhí)行到代碼塊時(shí),對(duì)應(yīng)的執(zhí)行上下文如下:
foo函數(shù)執(zhí)行上 | |
---|---|
變量環(huán)境 a = 1, c = 3 | 詞法環(huán)境 {b = 3, d = 5} {b = 2} |
當(dāng)代碼塊的代碼執(zhí)行完畢后,對(duì)應(yīng)的詞法環(huán)境里的變量就會(huì)被彈出棧
foo函數(shù)執(zhí)行上下文 | |
---|---|
變量環(huán)境 a = 1, c = 3 | 詞法環(huán)境 {b = 2} |
原因
通過上面的分析,我們可以總結(jié)變量提升和塊級(jí)作用域的實(shí)現(xiàn):
- 通過編譯階段,通過var聲明的變量已經(jīng)存在變量環(huán)境中并賦值為undefined,所以在執(zhí)行代碼的任何位置都能狗訪問得到,而不需要在聲明之后才能訪問
- 而通過let聲明的變量,會(huì)被存放在詞法環(huán)境中但并未初始化(不包含代碼塊的let或const聲明的變量),所以并不能訪問,而是等到遇到let聲明語句的時(shí)候才初始化并賦值
- 在遇到代碼塊時(shí),會(huì)先將let和const聲明的變量存放在詞法環(huán)境中并設(shè)置為初始化,如果此時(shí)在代碼塊中在let聲明變量之前使用該變量,并不會(huì)去外部作用域找該變量,因?yàn)榇藭r(shí)詞法作用域已經(jīng)存在改變量了,但未初始化,所以此時(shí)會(huì)報(bào)錯(cuò)誤,這也是let暫時(shí)性死區(qū)的原因
單個(gè)執(zhí)行上下文中變量的查找規(guī)則
沿著詞法環(huán)境的棧頂向下查詢,如果在詞法環(huán)境的某個(gè)快中查找到了,就直接返回給js引擎,如果沒有找到,就繼續(xù)在變量環(huán)境中查找
調(diào)用棧
調(diào)用棧是用來管理函數(shù)調(diào)用關(guān)系的一種棧結(jié)構(gòu)
在函數(shù)調(diào)用之前,會(huì)創(chuàng)建對(duì)應(yīng)的執(zhí)行上下文,并生成對(duì)應(yīng)的可執(zhí)行代碼
- js維護(hù)了一個(gè)棧結(jié)構(gòu),每當(dāng)遇到一個(gè)函數(shù)調(diào)用的時(shí)候,就創(chuàng)建一個(gè)執(zhí)行上下文,并壓入該棧中,
- 這個(gè)棧叫做執(zhí)行上下文棧,也叫做調(diào)用棧
- 當(dāng)函數(shù)執(zhí)行完畢之后,會(huì)將對(duì)應(yīng)的執(zhí)行上下文彈出棧結(jié)構(gòu)
- 棧的容量是有限的,當(dāng)棧容量不夠的時(shí)候就有可能發(fā)生棧溢出
作用域鏈
- 在函數(shù)中如果在當(dāng)前作用域中找不到所需要的變量,就得沿著作用域鏈往下去查找,直到找到為止
- 我們都知道,當(dāng)一段代碼在執(zhí)行的時(shí)候,會(huì)有對(duì)應(yīng)的執(zhí)行上下文,那變量沿著作用域鏈查看的規(guī)則也是在執(zhí)行上下文中設(shè)置的
- 在每個(gè)執(zhí)行上下文中,在變量環(huán)境中,都有一個(gè)外部引用,用來執(zhí)行外部的執(zhí)行上下文,我們把這個(gè)外部引用稱為outer
- 上文已經(jīng)說到,變量的查找首先會(huì)從執(zhí)行上下文的詞法環(huán)境中查找,找不到就在變量環(huán)境中查找,再找不到的話就會(huì)沿著outer去外部的執(zhí)行上下文中查找
- outer具體引用哪一個(gè)執(zhí)行上下文(作用域),是由詞法作用域決定的
詞法作用域
詞法作用域指的是作用域有代碼中函數(shù)的聲明位置決定的,也叫做靜態(tài)作用域
也就是說,當(dāng)創(chuàng)建一個(gè)執(zhí)行上下文的時(shí)候,其內(nèi)部的outer就會(huì)根據(jù)詞法作用域去執(zhí)行對(duì)應(yīng)的外部執(zhí)行上下文
在外部的執(zhí)行上下文中查找時(shí),也是先從詞法環(huán)境中開始
function fn() { console.log(a); } function fn1() { let a = 1 fn() } let a = 3 // let聲明的變量是在詞法環(huán)境中的 fn1() // 3
閉包
在js中,根據(jù)詞法作用域的規(guī)則,內(nèi)部函數(shù)總是可以訪問外部函數(shù)中聲明的變量,當(dāng)通過調(diào)用一個(gè)外部函數(shù)返回一個(gè)內(nèi)部函數(shù)后,即使該外部函數(shù)已經(jīng)執(zhí)行結(jié)束了,但是內(nèi)部函數(shù)引用外部函數(shù)的變量依然保存在內(nèi)存中,我們就把這些變量的集合稱為閉包
function foo() { var myName = " 極客時(shí)間 " 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())
上面代碼中,由于存在閉包現(xiàn)象,foo函數(shù)執(zhí)行結(jié)束后,內(nèi)部的變量還會(huì)被保存,調(diào)用棧如下圖:
當(dāng)執(zhí)行到 bar.setName 方法中的myName = "極客邦"這句代碼時(shí),JavaScript 引擎會(huì)沿著“當(dāng)前執(zhí)行上下文–>foo 函數(shù)閉包–> 全局執(zhí)行上下文”的順序來查找 myName 變量,你可以參考下面的調(diào)用棧狀態(tài)圖:
閉包的回收
- 如果閉包使用不正確,會(huì)很容易造成內(nèi)存泄漏的,關(guān)注閉包是如何回收的能讓你正確地使用閉包。
- 通常,如果引用閉包的函數(shù)是一個(gè)全局變量,那么閉包會(huì)一直存在直到頁面關(guān)閉;但如果這個(gè)閉包以后不再使用的話,就會(huì)造成內(nèi)存泄漏。
- 如果引用閉包的函數(shù)是個(gè)局部變量,等函數(shù)銷毀后,在下次 JavaScript 引擎執(zhí)行垃圾回收時(shí),判斷閉包這塊內(nèi)容如果已經(jīng)不再被使用了,那么 JavaScript 引擎的垃圾回收器就會(huì)回收這塊內(nèi)存。
- 所以在使用閉包的時(shí)候,你要盡量注意一個(gè)原則:如果該閉包會(huì)一直使用,那么它可以作為全局變量而存在;但如果使用頻率不高,而且占用內(nèi)存又比較大的話,那就盡量讓它成為一個(gè)局部變量。
var bar = { myName:"time.geekbang.com", printName: function () { console.log(myName) } } function foo() { let myName = " 極客時(shí)間 " return bar.printName } let myName = " 極客邦 " let _printName = foo() _printName() bar.printName()
從上下文角度講this
執(zhí)行上下文分為三種,對(duì)應(yīng)的this也只有三種:全局上下文的this,函數(shù)中的this,eval中的this
- 箭頭函數(shù)沒有自己的執(zhí)行上下文
- 全局上下文的this指向全局對(duì)象
- 函數(shù)上下文的this根據(jù)四種綁定規(guī)則判斷this指向
- 執(zhí)行上下文包含this指向
參考:https://time.geekbang.org/column/intro/100033601
相關(guān)文章
JS奇技之利用scroll來監(jiān)聽resize詳解
這篇文章主要給大家介紹了JS奇技之利用scroll來監(jiān)聽resize的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-06-06ES6記錄異步函數(shù)的執(zhí)行時(shí)間詳解
在這篇文章里,我會(huì)實(shí)現(xiàn)一個(gè)可重用的函數(shù)來處理 JavaScript 延時(shí)異步操作。有需要的小伙伴們可以參考借鑒,下面來一起看看。2016-08-08javascript結(jié)合ajax讀取txt文件內(nèi)容
這篇文章主要介紹了javascript結(jié)合ajax讀取txt文件內(nèi)容,方法非常簡(jiǎn)單,很實(shí)用,這里推薦給大家2014-12-12深入了解JavaScript中l(wèi)et/var/function的變量提升
這篇文章主要介紹了深入了解JavaScript中l(wèi)et/var/function的變量提升,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-07-07