<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從執(zhí)行上下文(ES3,ES5)的角度來(lái)理解"閉包"

          共 16654字,需瀏覽 34分鐘

           ·

          2021-05-29 23:01

          來(lái)源 | https://www.cnblogs.com/echoyya/p/14776868.html

          惰性十足,就是不愿意花時(shí)間把看過(guò)的東西整理一下,其它的任何事都比寫(xiě)博客要有吸引力,嗯... 要反省自己。
          今天看到一篇關(guān)于閉包的文章,里面有這樣一句話 “就我而言對(duì)于閉包的理解僅止步于一些概念,看到相關(guān)代碼知道這是個(gè)閉包,但閉包能解決哪些問(wèn)題場(chǎng)景我了解的并不多”,這說(shuō)的不就是我么,每每在面試中被問(wèn)及什么是閉包,大部分情況下得到的答復(fù)是(至少我以前是)A函數(shù)嵌套B函數(shù),B函數(shù)使用了A函數(shù)的內(nèi)部變量,且A函數(shù)返回B函數(shù),這就是閉包。
          而往往面試官想要聽(tīng)到的并不是這樣的答案,如果在多幾個(gè)這樣的回答,那么恭喜你,基本就涼了。
          在之前的面試中,關(guān)于閉包總是有種莫名的恐懼,想趕快結(jié)束這個(gè)話題,進(jìn)入下一環(huán)節(jié),有沒(méi)有?我原本想是深入學(xué)習(xí)一下閉包就好了,但經(jīng)過(guò)我多方考查學(xué)習(xí),發(fā)現(xiàn)閉包牽涉的知識(shí)點(diǎn)是很廣的,需要明白JS引擎的工作機(jī)制和一些底層的原理,了解了相關(guān)知識(shí)點(diǎn)之后,在回過(guò)頭理解閉包就容易多了。
          文章的最后,會(huì)介紹閉包的概念,形成、實(shí)現(xiàn),和使用,以及對(duì)性能和內(nèi)存的影響,其實(shí)還是很好理解的,學(xué)完這篇文章,至少可以讓你在下一次面試中,侃侃而談5分鐘吧。
          進(jìn)入正式主題

          介紹執(zhí)行上下文和執(zhí)行上下文棧概念

          JS中可執(zhí)行的代碼一共就分為三種:全局代碼、函數(shù)代碼、eval代碼。由于eval一般不會(huì)使用,這里不做討論。而代碼的執(zhí)行順序總是與代碼編寫(xiě)先后順序有所差異,先拋開(kāi)異步問(wèn)題,就算是同步代碼,它的執(zhí)行也與預(yù)期的不一致,這說(shuō)明代碼在執(zhí)行前一定發(fā)生了某些微妙的變化,JS引擎究竟做了什么呢?

          執(zhí)行上下文

          其實(shí)JS代碼在執(zhí)行前,JS引擎總要做一番準(zhǔn)備工作,這里的“準(zhǔn)備工作”,用個(gè)更專(zhuān)業(yè)一點(diǎn)的說(shuō)法,就叫做"執(zhí)行上下文(execution context)",對(duì)應(yīng)上述可執(zhí)行的代碼,會(huì)產(chǎn)生不同的執(zhí)行上下文
          1、全局執(zhí)行上下文:只有一個(gè),在客戶端中一般由瀏覽器創(chuàng)建,也就是window對(duì)象,能通過(guò)this直接訪問(wèn)到它。
          全局對(duì)象window上預(yù)定義了大量的方法和屬性,在全局環(huán)境的任意處都能直接訪問(wèn)這些屬性方法,同時(shí)window對(duì)象還是var聲明的全局變量的載體。我們通過(guò)var創(chuàng)建的全局對(duì)象,都可以通過(guò)window直接訪問(wèn)。
          2、函數(shù)執(zhí)行上下文:可存在無(wú)數(shù)個(gè),每當(dāng)一個(gè)函數(shù)被調(diào)用時(shí)都會(huì)創(chuàng)建一個(gè)函數(shù)上下文;需要注意的是,同一個(gè)函數(shù)被多次調(diào)用,都會(huì)創(chuàng)建一個(gè)新的上下文。

          執(zhí)行上下文棧

          那么接下來(lái)問(wèn)題來(lái)了,寫(xiě)的函數(shù)多了去了,如何管理創(chuàng)建的那么多執(zhí)行上下文呢? JavaScript 引擎創(chuàng)建了執(zhí)行上下文棧(Execution context stack,ECS)來(lái)管理執(zhí)行上下文。
          簡(jiǎn)稱執(zhí)行棧也叫調(diào)用棧,執(zhí)行棧用于存儲(chǔ)代碼執(zhí)行期間創(chuàng)建的所有上下文,具有FILO(First In Last Out先進(jìn)后出)的特性。
          JS代碼首次運(yùn)行,都會(huì)先創(chuàng)建一個(gè)全局執(zhí)行上下文并壓入到執(zhí)行棧中,之后每當(dāng)有函數(shù)被調(diào)用,都會(huì)創(chuàng)建一個(gè)新的函數(shù)執(zhí)行上下文并壓入棧內(nèi);由于執(zhí)行棧FILO的特性,所以可以理解為,JS代碼執(zhí)行完畢前在執(zhí)行棧底部永遠(yuǎn)有個(gè)全局執(zhí)行上下文
          棧中的執(zhí)行順序?yàn)椋合冗M(jìn)后出

          偽代碼模擬分析以下代碼中執(zhí)行上下文棧的行為

          function a() {  b()}
          function b() { c()}
          function c() { console.log('c');}a()

          定義一個(gè)數(shù)組來(lái)模擬執(zhí)行上下文棧的行為: ECStack = [];

          當(dāng) JavaScript 開(kāi)始要解釋執(zhí)行代碼時(shí),最先遇到肯定是全局代碼,所以初始化的時(shí)候首先就會(huì)向執(zhí)行上下文棧壓入一個(gè)全局執(zhí)行上下文,用 globalContext 表示它,并且只有當(dāng)整個(gè)應(yīng)用程序結(jié)束的時(shí)候,ECStack 才會(huì)被清空,所以程序結(jié)束之前, ECStack 最底部永遠(yuǎn)有個(gè) globalContext:

          ECStack = [    globalContext];

          執(zhí)行一個(gè)函數(shù),都會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,并且壓入執(zhí)行上下文棧中的棧頂,當(dāng)函數(shù)執(zhí)行完畢后,就會(huì)將該函數(shù)的執(zhí)行上下文從棧頂彈出。

          // 按照?qǐng)?zhí)行順序,分別創(chuàng)建對(duì)應(yīng)函數(shù)的執(zhí)行上下文,并且壓入執(zhí)行上下文棧的棧頂ECStack.push(functionAContext)    // push aECStack.push(functionBContext)    // push bECStack.push(functionCContext)    // push c
          // 棧執(zhí)行,首先C函數(shù)執(zhí)行完畢,先進(jìn)后出,ECStack.pop() // 彈出cECStack.pop() // 彈出bECStack.pop() // 彈出a
          // javascript接著執(zhí)行下面的代碼,但是ECStack底層永遠(yuǎn)有個(gè)globalContext,直到整個(gè)應(yīng)用程序結(jié)束的時(shí)候,ECStack 才會(huì)被清空// ......// ......

          代碼模擬實(shí)現(xiàn)棧的執(zhí)行過(guò)程

          class Stack {  constructor(){    this.items = []  }  push(ele) {    this.items.push(ele)  }  pop() {    return this.items.pop()  }}
          let stack = new Stack()stack.push(1)stack.push(2)stack.push(3)console.log(stack.pop()) // 3console.log(stack.pop()) // 2console.log(stack.pop()) // 1

          通過(guò)ES3提出的老概念—理解執(zhí)行上下文

          我在閱讀相關(guān)資料時(shí),遇到了一個(gè)問(wèn)題,就是關(guān)于執(zhí)行上下文說(shuō)法不一,不過(guò)大致可以分為兩種觀點(diǎn),一個(gè)是變量對(duì)象,活動(dòng)對(duì)象,詞法作用域,作用域鏈,另一個(gè)是詞法環(huán)境,變量環(huán)境,一番查閱可以確定的是,變量對(duì)象與活動(dòng)對(duì)象的概念是ES3提出的老概念,從ES5開(kāi)始就用詞法環(huán)境和變量環(huán)境替代了,因?yàn)楦媒忉尅?/span>

          先大致講一下變量對(duì)象,活動(dòng)對(duì)象,詞法作用域,作用域鏈吧

          1、變量對(duì)象和活動(dòng)對(duì)象

          變量對(duì)象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲(chǔ)了在上下文中定義的變量和函數(shù)聲明。不同執(zhí)行上下文中的變量對(duì)象不同,分別看一下全局上下文中的變量對(duì)象和函數(shù)上下文中的變量對(duì)象。

          1)、全局上下文中的變量對(duì)象

          全局上下文中的變量對(duì)象就是全局對(duì)象。W3School 中有介紹:

          • 全局對(duì)象是預(yù)定義的對(duì)象,作為 JavaScript 的全局函數(shù)和全局屬性的占位符。通過(guò)使用全局對(duì)象,可以訪問(wèn)所有其他所有預(yù)定義的對(duì)象、函數(shù)和屬性。

          • 在頂層 JavaScript 代碼中,可以用關(guān)鍵字 this 引用全局對(duì)象。因?yàn)槿謱?duì)象是作用域鏈的頭,這意味著所有非限定性的變量和函數(shù)名都會(huì)作為該對(duì)象的屬性來(lái)查詢。

          2)、函數(shù)上下文中的變量對(duì)象

          在函數(shù)上下文中用活動(dòng)對(duì)象(activation object, AO)來(lái)表示變量對(duì)象(VO)。

          活動(dòng)對(duì)象和變量對(duì)象其實(shí)是一個(gè)東西,只是變量對(duì)象是規(guī)范上的或者說(shuō)是引擎實(shí)現(xiàn)上的,不可在 JavaScript 環(huán)境中訪問(wèn),只有進(jìn)入一個(gè)執(zhí)行上下文中時(shí),這個(gè)執(zhí)行上下文的變量對(duì)象才會(huì)被激活,所以才叫活動(dòng)對(duì)象,而只有被激活的變量對(duì)象(也就是活動(dòng)對(duì)象)上的各種屬性才能被訪問(wèn)。

          換句話說(shuō):未進(jìn)入執(zhí)行階段之前,變量對(duì)象(VO)中的屬性都不能訪問(wèn)!進(jìn)入執(zhí)行階段之后,變量對(duì)象(VO)轉(zhuǎn)變?yōu)榱嘶顒?dòng)對(duì)象(AO),里面的屬性可以被訪問(wèn),并開(kāi)始進(jìn)行執(zhí)行階段的操作。它們其實(shí)都是同一個(gè)對(duì)象,只是處于執(zhí)行上下文的不同生命周期

          但是從嚴(yán)格角度來(lái)說(shuō),AO 實(shí)際上是包含了 VO 的。因?yàn)槌?VO 之外,AO 還包含函數(shù)的 parameters,以及 arguments 這個(gè)特殊對(duì)象。也就是說(shuō) AO 的確是在進(jìn)入到執(zhí)行階段的時(shí)候被激活,但是激活的除了 VO 之外,還包括函數(shù)執(zhí)行時(shí)傳入的參數(shù)和 arguments 這個(gè)特殊對(duì)象。
          AO = VO + function parameters + arguments
          活動(dòng)對(duì)象是在進(jìn)入函數(shù)上下文時(shí)刻被激活,通過(guò)函數(shù)的 arguments 屬性初始化。

          執(zhí)行上下文的代碼會(huì)分成兩個(gè)階段進(jìn)行處理,預(yù)解析和執(zhí)行:

          • 預(yù)解析的過(guò)程會(huì)激活A(yù)O對(duì)象,解析形參,變量提升及函數(shù)聲明等

          • 在代碼執(zhí)行階段,會(huì)從上到下順序執(zhí)行代碼,根據(jù)代碼,修改變量對(duì)象的值。

          2、詞法作用域

          作用域是指代碼中定義變量的區(qū)域。其規(guī)定了如何查找變量,也就是確定當(dāng)前執(zhí)行代碼對(duì)變量的訪問(wèn)權(quán)限。

          JavaScript 采用詞法作用域(lexical scoping),也就是靜態(tài)作用域,函數(shù)的作用域在函數(shù)定義的時(shí)候就決定了。

          詞法作用域根據(jù)源代碼中聲明變量的位置來(lái)確定該變量在何處可用。嵌套函數(shù)可訪問(wèn)聲明于它們外部作用域的變量

          // 詞法作用域var value = 1;
          function foo() { console.log(value);}
          function bar() { var value = 2; foo();}
          bar(); // 1

          分析下執(zhí)行過(guò)程:執(zhí)行 foo ,先從 foo 內(nèi)部查找是否有局部變量 value,如果沒(méi)有,就根據(jù)書(shū)寫(xiě)的位置,查找上一層作用域,也就是 value=1,所以打印 1。
          看個(gè)例子

          var scope = "global scope";function checkscope(){    var scope = "local scope";    function f(){        return scope;    }    return f();}checkscope();
          var scope = "global scope";function checkscope(){    var scope = "local scope";    function f(){        return scope;    }    return f;}checkscope()();

          兩段代碼都會(huì)打?。簂ocal scope。原因也很簡(jiǎn)單,因?yàn)镴avaScript采用的是詞法作用域,函數(shù)的作用域基于函數(shù)創(chuàng)建的位置。

          雖然兩段代碼執(zhí)行的結(jié)果一樣,但是兩段代碼究竟有什么不同呢?詞法作用域只是其中的一小部分,還有一個(gè)答案就是:執(zhí)行上下文棧的變化不一樣。

          模擬第一段代碼運(yùn)行時(shí)棧中的變化:

          ECStack.push(<checkscope> functionContext);ECStack.push(<f> functionContext);ECStack.pop();ECStack.pop();

          模擬第二段代碼運(yùn)行時(shí)棧中的變化:

          ECStack.push(<checkscope> functionContext);ECStack.pop();ECStack.push(<f> functionContext);ECStack.pop();

          3、作用域鏈

          每個(gè)函數(shù)都有自己的執(zhí)行上下文環(huán)境,當(dāng)代碼在這個(gè)環(huán)境中執(zhí)行時(shí),會(huì)創(chuàng)建變量對(duì)象的作用域鏈,作用域鏈類(lèi)似一個(gè)對(duì)象列表,它保證了變量對(duì)象的有序訪問(wèn)。

          作用域鏈的最前端是當(dāng)前代碼執(zhí)行環(huán)境的變量對(duì)象,也稱“活躍對(duì)象AO”,當(dāng)查找變量的時(shí)候,會(huì)先從當(dāng)前上下文的變量對(duì)象中查找,如果找到就停止查找,如果沒(méi)有就會(huì)繼續(xù)向上級(jí)作用域(父級(jí)執(zhí)行上下文的變量對(duì)象)查找,直到找到全局上下文的變量對(duì)象(全局對(duì)象)

          特別注意:作用域鏈的逐級(jí)查找,也會(huì)影響到程序的性能,變量作用域鏈越長(zhǎng)對(duì)性能影響越大,這也是為什么要盡量避免使用全局變量的一個(gè)主要原因。

          那么這個(gè)作用域鏈?zhǔn)窃趺葱纬傻哪兀?/span>

          這是因?yàn)楹瘮?shù)有一個(gè)內(nèi)部屬性 [[scope]]:當(dāng)函數(shù)創(chuàng)建時(shí),會(huì)保存所有父變量對(duì)象到其中,可以理解 [[scope]] 就是所有父變量對(duì)象的層級(jí)鏈,當(dāng)函數(shù)激活時(shí),進(jìn)入函數(shù)上下文,會(huì)將當(dāng)前激活的活動(dòng)對(duì)象添加到作用鏈的最前端。

          此時(shí)就可以理解,查找變量時(shí)首先找自己,沒(méi)有再找父親。

          下面以一個(gè)函數(shù)的創(chuàng)建和激活兩個(gè)時(shí)期來(lái)講解作用域鏈?zhǔn)侨绾蝿?chuàng)建和變化的。

          function foo() {    function bar() {        ...    }}

          函數(shù)創(chuàng)建時(shí),各自的[[scope]]為:

          foo.[[scope]] = [  globalContext.VO];
          bar.[[scope]] = [ fooContext.AO, globalContext.VO];

          當(dāng)函數(shù)激活時(shí),進(jìn)入函數(shù)上下文,就會(huì)將當(dāng)前激活的活動(dòng)對(duì)象添加到作用鏈的前端。

          這時(shí)候當(dāng)前的執(zhí)行上下文的作用域鏈為 Scope = [AO].concat([[Scope]]);

          以下面代碼為例,結(jié)合變量對(duì)象和執(zhí)行上下文棧,來(lái)總結(jié)一下函數(shù)執(zhí)行上下文中作用域鏈和變量對(duì)象的創(chuàng)建過(guò)程。

          var scope = "global scope";function checkscope(){    var scope2 = 'local scope';    return scope2;}checkscope();

          執(zhí)行過(guò)程如下(偽代碼):

          // 1.chec
          kscope 函數(shù)被創(chuàng)建,保存父變量對(duì)象到 內(nèi)部屬性[[scope]] checkscope.[[scope]] = [ globalContext.VO];
          // 2.執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,checkscope 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧ECStack = [ checkscopeContext, globalContext];
          // 3.checkscope 函數(shù)并不立刻執(zhí)行,開(kāi)始做準(zhǔn)備工作,第一步:復(fù)制函數(shù)[[scope]]屬性創(chuàng)建作用域鏈checkscopeContext = { Scope: checkscope.[[scope]],}
          // 4.用 arguments 創(chuàng)建活動(dòng)對(duì)象,隨后初始化活動(dòng)對(duì)象,加入形參、函數(shù)聲明、變量聲明checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]],}
          // 5.將活動(dòng)對(duì)象壓入 checkscope 作用域鏈Scope的頂端checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]]}
          // 6.準(zhǔn)備工作做完,開(kāi)始執(zhí)行函數(shù),隨著函數(shù)的執(zhí)行,修改 AO 的屬性值checkscopeContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]]}
          // 7.查找到 scope2 的值,返回后函數(shù)執(zhí)行完畢,函數(shù)上下文從執(zhí)行上下文棧中彈出ECStack = [ globalContext];

          4、活學(xué)活用 — 案例分析

          通過(guò)案例分析的形式,串聯(lián)上述所有知識(shí)點(diǎn),模擬執(zhí)行上下文創(chuàng)建執(zhí)行的過(guò)程

          var scope = "global scope";function checkscope(){  var scope = "local scope";  function f(){      return scope;  }  return f();}checkscope();
          // 1.執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文,全局上下文被壓入執(zhí)行上下文棧 ECStack = [ globalContext ];
          // 2.全局上下文初始化 globalContext = { VO: [global], Scope: [globalContext.VO], this: globalContext.VO }
          // 3.初始化的同時(shí),checkscope 函數(shù)被創(chuàng)建,保存作用域鏈到函數(shù)的內(nèi)部屬性[[scope]] checkscope.[[scope]] = [ globalContext.VO ];
          // 4.執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,并壓入執(zhí)行上下文棧 ECStack = [ checkscopeContext, globalContext ];
          // 5.checkscope 函數(shù)執(zhí)行上下文初始化:/** * 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈, * 用 arguments 創(chuàng)建活動(dòng)對(duì)象, * 初始化活動(dòng)對(duì)象,即加入形參、函數(shù)聲明、變量聲明, * 將活動(dòng)對(duì)象壓入 checkscope 作用域鏈頂端。 * 同時(shí) f 函數(shù)被創(chuàng)建,保存作用域鏈到 f 函數(shù)的內(nèi)部屬性[[scope]] */ checkscopeContext = { AO: { arguments: { length: 0 }, scope: undefined, f: reference to function f(){} // 引用函數(shù) }, Scope: [AO, globalContext.VO], this: undefined }
          // 6.執(zhí)行 f 函數(shù),創(chuàng)建 f 函數(shù)執(zhí)行上下文,f 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧 ECStack = [ fContext, checkscopeContext, globalContext ];
          // 7.f 函數(shù)執(zhí)行上下文初始化, 以下跟第 5 步相同: /** 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈 用 arguments 創(chuàng)建活動(dòng)對(duì)象 初始化活動(dòng)對(duì)象,即加入形參、函數(shù)聲明、變量聲明 將活動(dòng)對(duì)象壓入 f 作用域鏈頂端 */ fContext = { AO: { arguments: { length: 0 } }, Scope: [AO, checkscopeContext.AO, globalContext.VO], this: undefined }// 8.f 函數(shù)執(zhí)行,沿著作用域鏈查找 scope 值,返回 scope 值
          // 9.f 函數(shù)執(zhí)行完畢,f 函數(shù)上下文從執(zhí)行上下文棧中彈出 ECStack = [ checkscopeContext, globalContext ];
          // 10.checkscope 函數(shù)執(zhí)行完畢,checkscope 執(zhí)行上下文從執(zhí)行上下文棧中彈出 ECStack = [ globalContext ];

          通過(guò)ES5提出的新概念—理解執(zhí)行上下文

          執(zhí)行上下文創(chuàng)建分為創(chuàng)建階段與執(zhí)行階段兩個(gè)階段,較為難理解應(yīng)該是創(chuàng)建階段。
          創(chuàng)建階段主要負(fù)責(zé)三件事:

          • 確定this

          • 創(chuàng)建詞法環(huán)境(LexicalEnvironment)

          • 創(chuàng)建變量環(huán)境(VariableEnvironment)

          1、創(chuàng)建階段

          ExecutionContext = {      ThisBinding = <this value>,  // 確定this    LexicalEnvironment = {},     // 創(chuàng)建詞法環(huán)境    VariableEnvironment = {},    // 創(chuàng)建變量環(huán)境};

          1)、確定this

          官方稱呼為:This Binding,在全局執(zhí)行上下文中,this總是指向全局對(duì)象,例如瀏覽器環(huán)境下this指向window對(duì)象。

          而在函數(shù)執(zhí)行上下文中,this的值取決于函數(shù)的調(diào)用方式,如果被一個(gè)對(duì)象調(diào)用,那么this指向這個(gè)對(duì)象。否則this一般指向全局對(duì)象window或者undefined(嚴(yán)格模式)。

          2)、詞法環(huán)境

          詞法環(huán)境中包含標(biāo)識(shí)符和變量的映射關(guān)系,標(biāo)識(shí)符表示變量/函數(shù)的名稱,變量是對(duì)實(shí)際對(duì)象【包括函數(shù)類(lèi)型對(duì)象】或原始值的引用。

          詞法環(huán)境由環(huán)境記錄與外部環(huán)境引入記錄兩個(gè)部分組成。

          • 環(huán)境記錄:用于存儲(chǔ)當(dāng)前環(huán)境中的變量和函數(shù)聲明的實(shí)際位置;

          • 外部環(huán)境引入記錄:用于保存自身環(huán)境可以訪問(wèn)的其它外部環(huán)境,有點(diǎn)作用域鏈的意思

          那么全局執(zhí)行上下文與函數(shù)執(zhí)行上下文,也導(dǎo)致了詞法環(huán)境分為全局詞法環(huán)境和函數(shù)詞法環(huán)境兩種。

          • 全局詞法環(huán)境:對(duì)外部環(huán)境的引入記錄為 null,因?yàn)樗旧砭褪亲钔鈱迎h(huán)境,除此之外它還記錄了當(dāng)前環(huán)境下的所有屬性、方法位置。

          • 函數(shù)詞法環(huán)境:包含用戶在函數(shù)中定義的所有屬性方法外,還包含一個(gè)arguments對(duì)象(該對(duì)象包含了索引和傳遞給函數(shù)的參數(shù)之間的映射以及傳遞給函數(shù)的參數(shù)的長(zhǎng)度)。函數(shù)詞法環(huán)境的外部環(huán)境引入可以是全局環(huán)境,也可以是其它函數(shù)環(huán)境,這個(gè)根據(jù)實(shí)際代碼而定。

          環(huán)境記錄在全局和函數(shù)中也不同,全局中的環(huán)境記錄叫對(duì)象環(huán)境記錄,函數(shù)中環(huán)境記錄叫聲明性環(huán)境記錄,下方有展示:

          • 對(duì)象環(huán)境記錄: 用于定義在全局執(zhí)行上下文中出現(xiàn)的變量和函數(shù)的關(guān)聯(lián)。全局環(huán)境包含對(duì)象環(huán)境記錄。

          • 聲明性環(huán)境記錄 存儲(chǔ)變量、函數(shù)和參數(shù)。一個(gè)函數(shù)環(huán)境包含聲明性環(huán)境記錄。

          GlobalExectionContext = {    // 全局環(huán)境  LexicalEnvironment: {      // 全局詞法環(huán)境    EnvironmentRecord: {     // 類(lèi)型為對(duì)象環(huán)境記錄      Type: "Object",       // 標(biāo)識(shí)符綁定在這里     },    outer: < null >  }};
          FunctionExectionContext = { // 函數(shù)環(huán)境 LexicalEnvironment: { // 函數(shù)詞法環(huán)境 EnvironmentRecord: { // 類(lèi)型為聲明性環(huán)境記錄 Type: "Declarative", // 標(biāo)識(shí)符綁定在這里 }, outer: < Global or outerfunction environment reference > }};

          3)、變量環(huán)境

          它也是一個(gè)詞法環(huán)境,它具備詞法環(huán)境所有屬性,同樣有環(huán)境記錄與外部環(huán)境引入。

          在ES6中唯一的區(qū)別在于詞法環(huán)境用于存儲(chǔ)函數(shù)聲明與let const聲明的變量,而變量環(huán)境僅僅存儲(chǔ)var聲明的變量。

          let a = 20;  const b = 30;  var c;
          function multiply(e, f) { var g = 20; return e * f * g; }
          c = multiply(20, 30);
          上述代碼中執(zhí)行上下文的創(chuàng)建過(guò)程:
          //全局執(zhí)行上下文GlobalExectionContext = {    ThisBinding: Global Object,  // this綁定為全局對(duì)象    LexicalEnvironment: {          // 詞法環(huán)境      EnvironmentRecord: {                 Type: "Object",            // 對(duì)象環(huán)境記錄        // let const創(chuàng)建的變量a b在這        a:  uninitialized ,          b:  uninitialized ,          multiply: < func >        }      outer: null                // 全局環(huán)境外部環(huán)境引入為null    },
          VariableEnvironment: { // 變量環(huán)境 EnvironmentRecord: { Type: "Object", // 對(duì)象環(huán)境記錄 // var創(chuàng)建的c在這 c: undefined, } outer: null // 全局環(huán)境外部環(huán)境引入為null } }
          // 函數(shù)執(zhí)行上下文FunctionExectionContext = { ThisBinding: Global Object, //由于函數(shù)是默認(rèn)調(diào)用 this綁定同樣是全局對(duì)象 LexicalEnvironment: { // 詞法環(huán)境 EnvironmentRecord: { Type: "Declarative", // 聲明性環(huán)境記錄 // arguments對(duì)象在這 Arguments: {0: 20, 1: 30, length: 2}, }, outer: GlobalEnvironment // 外部環(huán)境引入記錄為Global },
          VariableEnvironment: { // 變量環(huán)境 EnvironmentRecord: { Type: "Declarative", // 聲明性環(huán)境記錄 // var創(chuàng)建的g在這 g: undefined }, outer: GlobalEnvironment // 外部環(huán)境引入記錄為Global } }

          這會(huì)引發(fā)我們另外一個(gè)思考,那就是變量提升的原因:

          我們會(huì)發(fā)現(xiàn)在創(chuàng)建階段,代碼會(huì)被掃描并解析變量和函數(shù)聲明,其中l(wèi)et 和 const 定義的變量沒(méi)有任何與之關(guān)聯(lián)的值,會(huì)保持未初始化的狀態(tài)。

          但 var 定義的變量設(shè)置為 undefined。

          所以這就是為什么可以在聲明之前,訪問(wèn)到 var 聲明的變量(盡管是 undefined),但如果在聲明之前訪問(wèn) let 和 const 聲明的變量就會(huì)報(bào)錯(cuò)的原因,也就是常說(shuō)的暫時(shí)性死區(qū),

          在執(zhí)行上下文創(chuàng)建階段,函數(shù)聲明與var聲明的變量在創(chuàng)建階段已經(jīng)被賦予了一個(gè)值,var聲明被設(shè)置為了undefined,函數(shù)被設(shè)置為了自身函數(shù),而let const被設(shè)置為未初始化。這是因?yàn)閳?zhí)行上下文創(chuàng)建階段JS引擎對(duì)兩者初始化賦值不同。

          執(zhí)行階段

          上下文除了創(chuàng)建階段外,還有執(zhí)行階段,代碼執(zhí)行時(shí)根據(jù)之前的環(huán)境記錄對(duì)應(yīng)賦值,比如早期var在創(chuàng)建階段為undefined,如果有值就對(duì)應(yīng)賦值,像let const值為未初始化,如果有值就賦值,無(wú)值則賦予undefined。

          執(zhí)行上下文總結(jié)

          1. 全局執(zhí)行上下文一般由瀏覽器創(chuàng)建,代碼執(zhí)行時(shí)就會(huì)創(chuàng)建;函數(shù)執(zhí)行上下文只有函數(shù)被調(diào)用時(shí)才會(huì)創(chuàng)建,同一個(gè)函數(shù)被多次調(diào)用,都會(huì)創(chuàng)建一個(gè)新的上下文。

          2. 調(diào)用棧用于存放所有執(zhí)行上下文,滿足FILO特性。

          3. 執(zhí)行上下文創(chuàng)建階段分為綁定this,創(chuàng)建詞法環(huán)境,變量環(huán)境三步,兩者區(qū)別在于詞法環(huán)境存放函數(shù)聲明與const let聲明的變量,而變量環(huán)境只存儲(chǔ)var聲明的變量。

          4. 詞法環(huán)境主要由環(huán)境記錄與外部環(huán)境引入記錄兩個(gè)部分組成,全局上下文與函數(shù)上下文的外部環(huán)境引入記錄不一樣,全局為null,函數(shù)為全局環(huán)境或者其它函數(shù)環(huán)境。環(huán)境記錄也不一樣,全局叫對(duì)象環(huán)境記錄,函數(shù)叫聲明性環(huán)境記錄。

          5. ES3之前的變量對(duì)象與活動(dòng)對(duì)象的概念在ES5之后由詞法環(huán)境,變量環(huán)境來(lái)解釋?zhuān)瑑烧吒拍畈粵_突,后者理解更為通俗易懂。

          閉包

          上文說(shuō)了這么多,其實(shí)我本意只是想聊一聊閉包的,終于回歸正題。

          閉包是什么?

          MDN 對(duì)閉包的定義簡(jiǎn)單理解就是:

          閉包是由函數(shù)以及聲明該函數(shù)的詞法環(huán)境組合而成的。該環(huán)境包含了這個(gè)閉包創(chuàng)建時(shí)作用域內(nèi)的任何局部變量(閉包維持了一個(gè)對(duì)它的詞法環(huán)境的引用:在一個(gè)函數(shù)內(nèi)部定義的函數(shù),會(huì)將外部函數(shù)的活躍對(duì)象添加到自己的作用域鏈中)。

          所以可以在一個(gè)內(nèi)層函數(shù)中訪問(wèn)到其外層函數(shù)的作用域。在 JavaScript 中,每當(dāng)創(chuàng)建一個(gè)函數(shù),閉包就會(huì)在函數(shù)創(chuàng)建的同時(shí)被創(chuàng)建出來(lái)。

          人們常說(shuō)的閉包無(wú)非就是:函數(shù)內(nèi)部返回一個(gè)函數(shù),一是可以讀取并操作函數(shù)內(nèi)部的變量,二是可以讓這些變量的值始終保存在內(nèi)存中。

          而在《JavaScript權(quán)威指南》中講到:從理論的角度講,所有的JavaScript函數(shù)都是閉包。:

          1. 從理論角度:所有的函數(shù)。因?yàn)樗鼈兌荚趧?chuàng)建時(shí)保存了上層上下文的數(shù)據(jù)。哪怕是簡(jiǎn)單的全局變量也是如此,因?yàn)楹瘮?shù)中訪問(wèn)全局變量就相當(dāng)于是在訪問(wèn)自由變量,這個(gè)時(shí)候使用最外層的作用域。

          2. 從實(shí)踐角度:閉包無(wú)非滿足以下兩點(diǎn):

            • 閉包首先得是一個(gè)函數(shù)。

            • 閉包能訪問(wèn)外部函數(shù)作用域中的自由變量,即使外部函數(shù)上下文已銷(xiāo)毀。(也可以理解為是自帶了執(zhí)行環(huán)境的函數(shù))

          閉包的形成與實(shí)現(xiàn)

          上文中介紹過(guò)JavaScript是采用詞法作用域的,講的是函數(shù)的執(zhí)行依賴于函數(shù)定義的時(shí)候所產(chǎn)生的變量作用域。

          為了去實(shí)現(xiàn)這種詞法作用域,JavaScript函數(shù)對(duì)象的內(nèi)部狀態(tài)不僅包含函數(shù)邏輯的代碼,還包含當(dāng)前作用域鏈的引用。

          函數(shù)對(duì)象可以通過(guò)這個(gè)作用域鏈相互關(guān)聯(lián)起來(lái),函數(shù)體內(nèi)部的變量都可以保存在函數(shù)的作用域內(nèi)

          let scope = "global scope";function checkscope() {    let scope = "local scope";   // 自由變量    function f() {    // 閉包        console.log(scope);    };    return f;};
          let foo = checkscope();foo();
          // 1. 偽代碼分別表示執(zhí)行棧中上下文的變化,以及上下文創(chuàng)建的過(guò)程,首先執(zhí)行棧中永遠(yuǎn)都會(huì)存在一個(gè)全局執(zhí)行上下文。ECStack = [GlobalExecutionContext];
          // 2. 此時(shí)全局上下文中存在兩個(gè)變量scope、foo與一個(gè)函數(shù)checkscope,上下文用偽代碼表示具體是這樣:GlobalExecutionContext = { // 全局執(zhí)行上下文 ThisBinding: Global Object , LexicalEnvironment: { // 詞法環(huán)境 EnvironmentRecord: { Type: "Object", // 對(duì)象環(huán)境記錄 scope: uninitialized , foo: uninitialized , checkscope: func } outer: null // 全局環(huán)境外部環(huán)境引入為null }}// 3. 全局上下文創(chuàng)建階段結(jié)束,進(jìn)入執(zhí)行階段,全局執(zhí)行上下文的標(biāo)識(shí)符中像scope、foo之類(lèi)的變量被賦值,然后開(kāi)始執(zhí)行checkscope函數(shù),于是一個(gè)新的函數(shù)執(zhí)行上下文被創(chuàng)建,并壓入執(zhí)行棧中:ECStack = [checkscopeExecutionContext,GlobalExecutionContext];
          // 4. checkscope函數(shù)執(zhí)行上下文進(jìn)入創(chuàng)建階段:checkscopeExecutionContext = { // 函數(shù)執(zhí)行上下文 ThisBinding: Global Object, LexicalEnvironment: { // 詞法環(huán)境 EnvironmentRecord: { Type: "Declarative", // 聲明性環(huán)境記錄 Arguments: {}, scope: uninitialized , f: func }, outer: GlobalLexicalEnvironment // 外部環(huán)境引入記錄為<Global> }}
          // 5. checkscope() 等同于window.checkscope() ,所以checkExectionContext 中this指向全局,而且外部環(huán)境引用outer也指向了全局(作用域鏈),其次在標(biāo)識(shí)符中記錄了arguments對(duì)象以及變量scope與函數(shù)f// 6. 函數(shù) checkscope 執(zhí)行到返回函數(shù) f 時(shí),函數(shù)執(zhí)行完畢,checkscope 的執(zhí)行上下文被彈出執(zhí)行棧,所以此時(shí)執(zhí)行棧中又只剩下全局執(zhí)行上下文:ECStack = [GlobalExecutionContext];
          // 7. 代碼foo()執(zhí)行,創(chuàng)建foo的執(zhí)行上下文,ECStack = [fooExecutionContext, GlobalExecutionContext];
          // 8. foo的執(zhí)行上下文是這樣:fooExecutionContext = { ThisBinding: Global Object , LexicalEnvironment: { // 詞法環(huán)境 EnvironmentRecord: { Type: "Declarative", // 聲明性環(huán)境記錄 Arguments: {}, }, outer: checkscopeEnvironment // 外部環(huán)境引入記錄為<checkscope> }}// 9. foo()等同于window.foo(),所以this指向全局window,但outer外部環(huán)境引入有點(diǎn)不同,指向了外層函數(shù) checkscope(原因是JS采用詞法作用域,也就是靜態(tài)作用域,函數(shù)的作用域在定義時(shí)就確定了,而不是執(zhí)行時(shí)確定)/** * a. 但是可以發(fā)現(xiàn)的是,現(xiàn)在執(zhí)行棧中只有 fooExecutionContext 和 GlobalExecutionContext, checkscopeExecutionContext 在執(zhí)行完后就被釋放了,怎么還能訪問(wèn)到 其中的變量? * b. 正常來(lái)說(shuō)確實(shí)是不可以,但是因?yàn)殚]包 foo 外部環(huán)境 outer 的引用,從而讓 checkscope作用域中的變量依舊存活在內(nèi)存中,無(wú)法被釋放,所以有時(shí)有必要手動(dòng)釋放自由變量。 * c. 總結(jié)一句,閉包是指能使用其它作用域自由變量的函數(shù),即使作用域已銷(xiāo)毀。 */

          閉包有什么用?

          說(shuō)閉包聊閉包,結(jié)果閉包有啥用都不知道,甚至遇到了一個(gè)閉包第一時(shí)間都沒(méi)反應(yīng)過(guò)來(lái)這是閉包,說(shuō)說(shuō)閉包有啥用:

          1)、模擬私有屬性、方法

          所謂私有屬性方法其實(shí)就是這些屬性方法只能被同一個(gè)類(lèi)中的其它方法所調(diào)用,但是JavaScript中并未提供專(zhuān)門(mén)用于創(chuàng)建私有屬性的方法,但可以通過(guò)閉包模擬它:
          私有方法不僅有利于限制對(duì)代碼的訪問(wèn):還提供了管理全局命名空間的強(qiáng)大能力,避免非核心的方法弄亂了代碼的公共接口部分。

          例一:通過(guò)自執(zhí)行函數(shù)返回了一個(gè)對(duì)象,只創(chuàng)建了一個(gè)詞法環(huán)境,為三個(gè)閉包函數(shù)所共享:fn.increment,fn.decrement 和 fn.value,除了這三個(gè)方法能訪問(wèn)到變量privateCounter和 changeBy函數(shù)外,無(wú)法再通過(guò)其它手段操作它們。

          let fn = (function () {  var privateCounter = 0;
          function changeBy(val) { privateCounter += val; }; return { increment: function () { changeBy(1); }, decrement: function () { changeBy(-1); }, value: function () { console.log(privateCounter); } };})();fn.value(); // 0fn.increment();fn.increment();fn.value(); // 2fn.decrement();fn.value(); // 1

          例二:構(gòu)造函數(shù)中也有閉包:

          function Echo(name) {  var age = 26;       // 私有屬性  this.name = name;   // 構(gòu)造器屬性  this.hello = function () {      console.log(`我的名字是${this.name},我今年${age}了`);  };};var person = new Echo('yya');person.hello();    //我的名字是yya,我今年26了

          若某個(gè)屬性方法在所有實(shí)例中都需要使用,一般都會(huì)推薦加在構(gòu)造函數(shù)的原型上,還有種做法就是利用私有屬性。

          比如這個(gè)例子中所有實(shí)例都可以正常使用變量 age,將age稱為私有屬性的同時(shí),也會(huì)將this.hello稱為特權(quán)方法,因?yàn)橹挥型ㄟ^(guò)這個(gè)方法才能訪問(wèn)被保護(hù)的私有屬性age。

          2)、工廠函數(shù)

          使用函數(shù)工廠創(chuàng)建了兩個(gè)新函數(shù) — 一個(gè)將其參數(shù)和 5 求和,另一個(gè)和 10 求和。 add5 和 add10 都是閉包。

          它們共享相同的函數(shù)定義,但是保存了不同的詞法環(huán)境。在 add5 的環(huán)境中,x 為 5。在 add10 中,x 則為 10。

          利用了閉包自帶執(zhí)行環(huán)境的特性(即使外層作用域已銷(xiāo)毀),僅僅使用一個(gè)形參完成了兩個(gè)形參求和。

          當(dāng)然例子函數(shù)還有個(gè)更專(zhuān)業(yè)的名詞,叫函數(shù)柯里化。

          function makeAdder(x) {    return function (y) {        console.log(x + y);    };};
          var add5 = makeAdder(5);var add10 = makeAdder(10);add5(2); // 7add10(2); // 12

          閉包對(duì)性能和內(nèi)存的影響

          1. 閉包會(huì)額外附帶函數(shù)的作用域(內(nèi)部匿名函數(shù)攜帶外部函數(shù)的作用域),會(huì)比其它函數(shù)多占用些內(nèi)存空間,過(guò)度的使用可能會(huì)導(dǎo)致內(nèi)存占用的增加。

          2. 閉包中包含與函數(shù)執(zhí)行上下文相同的作用域鏈引用,因此會(huì)產(chǎn)生一定的負(fù)面作用,當(dāng)函數(shù)中活躍對(duì)象和執(zhí)行上下文銷(xiāo)毀時(shí),由于閉包仍存在對(duì)活躍對(duì)象的引用,導(dǎo)致活躍對(duì)象無(wú)法銷(xiāo)毀,可能會(huì)導(dǎo)致內(nèi)存泄漏。

          3. 閉包中如果存在對(duì)外部變量的訪問(wèn),會(huì)增加標(biāo)識(shí)符的查找路徑,在一定的情況下,也會(huì)造成性能方面的損失。解決此類(lèi)問(wèn)題的辦法:盡量將外部變量存入到局部變量中,減少作用域鏈的查找長(zhǎng)度。

          綜上所述:如果不是某些特定任務(wù)需要使用閉包,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的,因?yàn)樵谔幚硭俣群蛢?nèi)存消耗方面對(duì)腳本性能具有負(fù)面影響。

          了解了JS引擎的工作機(jī)制之后,我們不能只停留在理解概念的層面,而要將其作為基礎(chǔ)工具,用以優(yōu)化和改善我們?cè)趯?shí)際工作中的代碼,提高執(zhí)行效率,產(chǎn)生實(shí)際價(jià)值才是我們真正的目的。

          就拿變量查找機(jī)制來(lái)說(shuō),如果代碼嵌套很深,每引用一次全局變量,JS引擎就要查找整個(gè)作用域鏈,比如處于作用域鏈的最底端window和document對(duì)象就存在這個(gè)問(wèn)題,因此我們圍繞這個(gè)問(wèn)題可以做很多性能優(yōu)化的工作,當(dāng)然還有其他方面的優(yōu)化,此處不再贅述,如果有幫到你,點(diǎn)個(gè)贊再走吧~~~

          學(xué)習(xí)更多技能

          請(qǐng)點(diǎn)擊下方公眾號(hào)

          瀏覽 57
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲三高青在线观看免费 | 久久黄色五月 | 日本操逼小视频 | 亚洲日韩国产AV无码无码精品 | 欧美性猛交XXXXX无码婷 日日嗨av一区二区三区免费 |