【五】JS執(zhí)行

前言
本篇章是偏理解性的博客,主要講述在js環(huán)境中變量、方法執(zhí)行方式。理解執(zhí)行順序,能夠更好地幫助你在開(kāi)發(fā)中解決奇奇怪怪的問(wèn)題。
面試回答
1.執(zhí)行上下文:執(zhí)行上下文可以簡(jiǎn)單理解成一個(gè)對(duì)象,這個(gè)對(duì)象包含變量對(duì)象、作用域鏈、this指向,一般就全局執(zhí)行上下文和函數(shù)執(zhí)行上下文。
2.變量提升:變量提升就是在賦值操作之前,就使用對(duì)應(yīng)的變量,導(dǎo)致變量變成undefined。原因在于執(zhí)行過(guò)程中,首先會(huì)建立活動(dòng)對(duì)象,然后構(gòu)建作用域鏈,再確定this指向,最后才是代碼執(zhí)行。創(chuàng)建變量或者函數(shù)的步驟都在建立活動(dòng)對(duì)象階段,而賦值操作是在代碼執(zhí)行階段,所以才會(huì)找不到。
3.this:this永遠(yuǎn)指向函數(shù)運(yùn)行時(shí)所在的對(duì)象,而不是函數(shù)被創(chuàng)建時(shí)所在的對(duì)象。改變this指向通常有三種方法,bind、call、apply,bind會(huì)返回一個(gè)新函數(shù) ,call在改變this指向后還執(zhí)行了函數(shù),且能夠接收多個(gè)參數(shù),而apply與call的區(qū)別在于apply接收數(shù)組作為傳入?yún)?shù)。
4.手寫(xiě)apply:首先判斷傳入的參數(shù)是否為值類(lèi)型,如果是值類(lèi)型,則直接返回該值類(lèi)型,如果是引用類(lèi)型,則給該參數(shù)添加fn屬性用來(lái)保存當(dāng)前this,這個(gè)this指向當(dāng)前的調(diào)用函數(shù)。下一步判斷是否存在其他參數(shù),如果有就將它展開(kāi),并將它作為參數(shù)傳入到上面的this函數(shù)中,并把執(zhí)行結(jié)果保存到result里,然后刪除fn屬性,并返回result。至于call與apply的區(qū)別在于傳入的參數(shù)不一樣,bind與apply的區(qū)別在于bind返回一個(gè)新函數(shù)并不執(zhí)行。
5.事件循環(huán)(Event Loop):事件循環(huán)是瀏覽器的一種解決JS單線程運(yùn)行時(shí)不阻塞的機(jī)制,具體流程是這樣的:1、首先所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧。2、如果遇到了異步任務(wù),就丟到主線程外的任務(wù)隊(duì)列,等異步任務(wù)有結(jié)果后,就會(huì)轉(zhuǎn)移到調(diào)用棧中。3、再然后執(zhí)行棧中所有同步任務(wù)執(zhí)行完畢,就會(huì)讀取調(diào)用棧,如果有任務(wù)就丟到執(zhí)行棧,開(kāi)始執(zhí)行這個(gè)任務(wù)中同步任務(wù)。4、最后主線程不斷重復(fù)上面的幾個(gè)步驟,這就是事件循環(huán)的一個(gè)機(jī)制。從執(zhí)行順序上來(lái)看,就是一個(gè)主線程 > 微任務(wù) > 宏任務(wù),有結(jié)果 > 宏任務(wù),無(wú)結(jié)果的順序。
知識(shí)點(diǎn)
javascript函數(shù)執(zhí)行過(guò)程主要由創(chuàng)建執(zhí)行環(huán)境、進(jìn)入函數(shù)調(diào)用棧、執(zhí)行、銷(xiāo)毀這四個(gè)階段構(gòu)成,下面我們來(lái)一一理解每一個(gè)階段所做的事情。
1.創(chuàng)建執(zhí)行環(huán)境
執(zhí)行環(huán)境,也就是執(zhí)行上下文,分為全局環(huán)境、函數(shù)環(huán)境、Eval 函數(shù)執(zhí)行環(huán)境。
全局環(huán)境指的是JS默認(rèn)的代碼執(zhí)行環(huán)境,是最外圍的一個(gè)執(zhí)行環(huán)境,在web瀏覽器中,全局執(zhí)行環(huán)境被認(rèn)為是window對(duì)象。一旦代碼被載入,引擎最先進(jìn)入的是這個(gè)環(huán)境,全局環(huán)境不會(huì)被自動(dòng)回收,只有在關(guān)閉瀏覽器窗口的時(shí)候才會(huì)被銷(xiāo)毀,所以在定義全局變量一定要格外小心。
函數(shù)環(huán)境是一個(gè)相對(duì)于全局環(huán)境的概念,由于在執(zhí)行代碼時(shí),線程就是在全局環(huán)境和函數(shù)環(huán)境之間來(lái)回穿梭的,可以簡(jiǎn)單理解為函數(shù)環(huán)境即任何一個(gè)函數(shù)被調(diào)用都會(huì)創(chuàng)建一個(gè)新的執(zhí)行環(huán)境,執(zhí)行結(jié)束后返回全局環(huán)境,而創(chuàng)建的函數(shù)環(huán)境等待垃圾回收。
Eval 函數(shù)執(zhí)行環(huán)境不經(jīng)常用,盡量避免,這里不做討論。
2.函數(shù)調(diào)用棧
在創(chuàng)建執(zhí)行環(huán)境后,函數(shù)/代碼下一個(gè)階段會(huì)被放入一個(gè)棧中,這個(gè)棧被稱(chēng)為函數(shù)調(diào)用棧。js根據(jù)函數(shù)的調(diào)用(執(zhí)行)來(lái)決定執(zhí)行順序。函數(shù)調(diào)用棧的棧底永遠(yuǎn)都是全局環(huán)境,而棧頂就是當(dāng)前正在執(zhí)行函數(shù)的環(huán)境。當(dāng)棧頂?shù)膱?zhí)行環(huán)境執(zhí)行完之后,就會(huì)出棧,并把執(zhí)行權(quán)交給之前的執(zhí)行環(huán)境。舉例:
1.首先 A() ;A 函數(shù)執(zhí)行了,A執(zhí)行環(huán)境入棧。
2.A函數(shù)執(zhí)行時(shí),又調(diào)用了 B(),B又執(zhí)行了,B入棧。
3.B中沒(méi)有可執(zhí)行的函數(shù)了,B執(zhí)行完出棧。
4.繼續(xù)執(zhí)行A, A中沒(méi)有可執(zhí)行的函數(shù)了,A執(zhí)行完 出棧。

上述例子只是為了闡明函數(shù)調(diào)用棧的作用,具體的執(zhí)行過(guò)程,包括執(zhí)行上下文、作用域鏈、this指向等操作是在執(zhí)行過(guò)程發(fā)生的。
3.執(zhí)行過(guò)程
當(dāng)函數(shù)被調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)新的函數(shù)執(zhí)行環(huán)境,該創(chuàng)建過(guò)程主要由兩個(gè)階段組成:建立階段 、代碼執(zhí)行階段
A. 建立階段(發(fā)生在調(diào)用/執(zhí)行一個(gè)函數(shù)時(shí),但是在執(zhí)行函數(shù)內(nèi)部的具體代碼之前) ? ? ??
1.建立活動(dòng)對(duì)象 ? ? ?
2.構(gòu)建作用域鏈 ? ? ??
3.確定this的指向
B. 代碼執(zhí)行階段 ? ? ??
1.執(zhí)行函數(shù)內(nèi)部的具體代碼
接下來(lái),我們逐個(gè)理解其中的步驟:
A.1. ?建立活動(dòng)對(duì)象
這里我們首先理解兩個(gè)概念:變量對(duì)象(Variable object,VO) 、活動(dòng)對(duì)象(Activation object)
變量對(duì)象(Variable object,VO) 是一個(gè)與執(zhí)行上下文相關(guān)的特殊對(duì)象,在函數(shù)上下文中,VO是不能直接訪問(wèn)的。變量對(duì)象用來(lái)存儲(chǔ)上下文的函數(shù)聲明、 函數(shù)形參、變量聲明。優(yōu)先級(jí):函數(shù)聲明>函數(shù)的形參>變量。
函數(shù)聲明:每找到一個(gè)函數(shù)聲明,就在活動(dòng)對(duì)象下面用函數(shù)名建立一個(gè)屬性,屬性值就是指向該函數(shù)在內(nèi)存中的地址的一個(gè)引用,如果上述函數(shù)名已經(jīng)存在于活動(dòng)對(duì)象下,那么則會(huì)被新的函數(shù)引用所覆蓋。
函數(shù)形參:建立arguments對(duì)象,檢查當(dāng)前上下文中的參數(shù),建立該對(duì)象下的屬性以及屬性值 。沒(méi)有實(shí)參的話,屬性值為undefined。
變量聲明:每找到一個(gè)變量聲明,就在活動(dòng)對(duì)象下面用變量名建立一個(gè)屬性,該屬性值為undefined。如果變量名稱(chēng)跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會(huì)干擾已經(jīng)存在的這類(lèi)屬性。
活動(dòng)對(duì)象(Activation object,AO):由于變量對(duì)象不能訪問(wèn),在函數(shù)執(zhí)行階段,由變量對(duì)象轉(zhuǎn)化而來(lái)的可訪問(wèn)對(duì)象。
舉例:
解析:我這邊的理解跟參考資料有所不同,望指正。
塊級(jí)作用域,簡(jiǎn)單來(lái)說(shuō)就是函數(shù)內(nèi)部和{}之間的部分。變量提升也是在建立階段產(chǎn)生的問(wèn)題,即在賦值操作之前(賦值操作在代碼執(zhí)行階段),就使用對(duì)應(yīng)的變量,從而使變量為undefined,舉例:
參考資料:https://article.itxueyuan.com/O0mA6
A.2.構(gòu)建作用域鏈
作用域鏈的最前端,始終都是當(dāng)前執(zhí)行的代碼所在函數(shù)的活動(dòng)對(duì)象。下一個(gè)活動(dòng)對(duì)象(AO)為包含本函數(shù)的外部函數(shù)的AO,以此類(lèi)推。最末端,為全局環(huán)境的變量對(duì)象。
注意:
1.雖然作用域鏈?zhǔn)窃诤瘮?shù)調(diào)用時(shí)構(gòu)建的,但是它跟調(diào)用順序(進(jìn)入調(diào)用棧的順序)無(wú)關(guān),因?yàn)樗桓P(guān)系(函數(shù)、包含函數(shù)的嵌套關(guān)系)有關(guān)。
2.作用域鏈?zhǔn)莿?chuàng)建函數(shù)的時(shí)候就創(chuàng)建了,此時(shí)的鏈只有全局變量對(duì)象,保存在函數(shù)的[[Scope]]屬性中,然后函數(shù)執(zhí)行時(shí)的,只是通過(guò)復(fù)制該屬性中的對(duì)象來(lái)構(gòu)建作用域鏈。
舉例:
b函數(shù)被a函數(shù)包含,a函數(shù)被window全局環(huán)境包含。
參考資料:https://blog.csdn.net/weixin_33919950/article/details/89625339
A.3.確定this的指向
this的指向在函數(shù)定義的時(shí)候是確定不了的,只有函數(shù)被調(diào)用的時(shí)候才能確定,并且this的最終指向的是那個(gè)調(diào)用它的對(duì)象 。
情況1:匿名函數(shù)
匿名函數(shù)this的默認(rèn)指向?yàn)閣indows
情況2:函數(shù)調(diào)用
1.如果一個(gè)函數(shù)中有this,但是它沒(méi)有被上一級(jí)的對(duì)象所調(diào)用,那么this指向的就是window。
2.如果一個(gè)函數(shù)中有this,這個(gè)函數(shù)有被上一級(jí)的對(duì)象所調(diào)用,那么this指向的就是上一級(jí)的對(duì)象。
3.如果一個(gè)函數(shù)中有this,這個(gè)函數(shù)中包含多個(gè)對(duì)象,盡管這個(gè)函數(shù)是被最外層的對(duì)象所調(diào)用,this指向的也只是它上一級(jí)的對(duì)象,也就是說(shuō)this指向的是直接調(diào)用它的對(duì)象。
情況3:構(gòu)造函數(shù)中的this指向
首先new關(guān)鍵字會(huì)創(chuàng)建一個(gè)空的對(duì)象,然后會(huì)自動(dòng)調(diào)用一個(gè)函數(shù)apply方法,將this指向這個(gè)空對(duì)象,這樣的話函數(shù)內(nèi)部的this就會(huì)被這個(gè)空的對(duì)象替代,可以參考后續(xù)關(guān)于new操作符的知識(shí)點(diǎn)。
當(dāng)構(gòu)造函數(shù)的this碰到return時(shí):如果返回值是一個(gè)對(duì)象,那么this指向的就是那個(gè)返回的對(duì)象,如果返回值不是一個(gè)對(duì)象那么this還是指向函數(shù)的實(shí)例。
情況4:箭頭函數(shù)的this指向
箭頭函數(shù)的this指向,是指向箭頭函數(shù)被創(chuàng)建時(shí)外部作用域(要么是window,要么是最近一層的局部函數(shù))的this指向的對(duì)象,而不是調(diào)用時(shí)指定this指向。舉例:
情況5:call、bind、apply的this指向
由于js中this的指向受函數(shù)運(yùn)行環(huán)境、調(diào)用的影響,指向經(jīng)常改變,使得開(kāi)發(fā)變得困難和模糊,所以在寫(xiě)一些復(fù)雜函數(shù)的時(shí)候經(jīng)常會(huì)用到this指向綁定,以避免出現(xiàn)不必要的問(wèn)題,call、apply、bind基本都能實(shí)現(xiàn)這一功能。
1.bind:bind用于將函數(shù)體內(nèi)的this綁定到某個(gè)對(duì)象,然后返回一個(gè)新函數(shù)
2.call:call方法可以指定this?的指向,然后再指定的作用域中,執(zhí)行函數(shù)。call可以接受多個(gè)參數(shù),第一個(gè)參數(shù)是this指向的對(duì)象,之后的是函數(shù)回調(diào)所需的入?yún)?
3.apply:apply?和call作用類(lèi)似,也是改變this指向,然后調(diào)用該函數(shù),唯一區(qū)別是apply接收數(shù)組作為函數(shù)執(zhí)行時(shí)的參數(shù)
PS:call、bind、apply能實(shí)現(xiàn)以下基礎(chǔ)功能,
間接調(diào)用函數(shù),改變this,劫持其他對(duì)象的方法
兩個(gè)函數(shù)實(shí)現(xiàn)繼承
為類(lèi)數(shù)組(arguments和nodeList)添加數(shù)組方法,如push、pop
(function(){
?Array.prototype.push.call(arguments,'王五');
?console.log(arguments);//['張三','李四','王五']
})('張三','李四')合并數(shù)組、求數(shù)組內(nèi)最大值
4.面試題:手動(dòng)實(shí)現(xiàn)bind、call、apply
實(shí)現(xiàn)call(obj,arg,arg....)1.改變this的指向。2.傳入?yún)?shù)。3.返回函數(shù)執(zhí)行結(jié)果
實(shí)現(xiàn)bind代碼:
B.1.執(zhí)行函數(shù)內(nèi)部的具體代碼
執(zhí)行代碼階段最主要有兩個(gè)事情:變量賦值、JS事件機(jī)制。其中變量賦值比較好理解,這里不做過(guò)多解釋?zhuān)酉聛?lái)著重理解JS事件機(jī)制。
JavaScript是單線程指的是同一時(shí)間只能干一件事情,只有前面的事情執(zhí)行完,才能執(zhí)行后面的事情。導(dǎo)致遇到耗時(shí)的任務(wù)時(shí)后面的代碼無(wú)法執(zhí)行,因此有了同步、異步任務(wù)。
同步任務(wù):for循環(huán)、new Promise等除了異步任務(wù)外的其他任務(wù)
異步任務(wù):微任務(wù)、宏任務(wù),其中微任務(wù)有Promise.then、process.nextTick、queueMicrotask,宏任務(wù)有setTimeout、setInterval、IO、UI渲染等
async/await只是一個(gè)語(yǔ)法糖,只是幫助我們返回一個(gè)Promise而已,如果方法被調(diào)用的話,async相當(dāng)于new Promise ,await后面的代碼相當(dāng)于.then里的代碼,且.then代碼必須得在promise中resolve后才會(huì)執(zhí)行,否則.then代碼不會(huì)執(zhí)行,await在async代碼執(zhí)行完畢后,進(jìn)入微任務(wù)隊(duì)列。
了解任務(wù)的概念后,現(xiàn)在整理一下任務(wù)執(zhí)行的流程:
1、所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧(execution context stack)。
2、主線程之外,還存在一個(gè)任務(wù)隊(duì)列(task queue)。只要是異步任務(wù),就丟到這個(gè)任務(wù)隊(duì)列,有了運(yùn)行結(jié)果,就在任務(wù)隊(duì)列之中放置一個(gè)事件。
3、當(dāng)任務(wù)隊(duì)列中的異步任務(wù)有了結(jié)果之后,就會(huì)將任務(wù)移動(dòng)到調(diào)用棧中。
4、一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取調(diào)用棧,開(kāi)始執(zhí)行該異步任務(wù)中同步任務(wù)。
5、主線程不斷重復(fù)上面的第三步,稱(chēng)為事件循環(huán)(Event Loop)。
總結(jié)執(zhí)行順序:主線程(同步任務(wù),new Promise)> Promise.then(微任務(wù))> setTimeout(宏任務(wù),有結(jié)果) > setTimeout(宏任務(wù),無(wú)結(jié)果),同類(lèi)型任務(wù)按照先進(jìn)先出的順序執(zhí)行。理清流程可得:

面試題:
4.銷(xiāo)毀執(zhí)行環(huán)境和活動(dòng)對(duì)象
某個(gè)執(zhí)行環(huán)境所有代碼執(zhí)行完之后,該環(huán)境被銷(xiāo)毀,保存在其中的所有變量和函數(shù)定義也隨之銷(xiāo)毀,這邊閉包有所不同,將會(huì)在下篇博客中,進(jìn)一步理解。全局執(zhí)行環(huán)境只會(huì)在關(guān)了瀏覽器或者程序的時(shí)候才被銷(xiāo)毀。
最后
走過(guò)路過(guò),不要錯(cuò)過(guò),點(diǎn)贊、收藏、評(píng)論三連~