天天看點

從執行上下文(ES3,ES5)的角度來了解

從執行上下文(ES3,ES5)的角度來了解

惰性十足,就是不願意花時間把看過的東西整理一下,其它的任何事都比寫部落格要有吸引力,嗯... 要檢討自己。

今天看到一篇關于閉包的文章,裡面有這樣一句話 “就我而言對于閉包的了解僅止步于一些概念,看到相關代碼知道這是個閉包,但閉包能解決哪些問題場景我了解的并不多”,這說的不就是我麼,每每在面試中被問及什麼是閉包,大部分情況下得到的答複是(至少我以前是)A函數嵌套B函數,B函數使用了A函數的内部變量,且A函數傳回B函數,這就是閉包。

而往往面試官想要聽到的并不是這樣的答案,如果在多幾個這樣的回答,那麼恭喜你,基本就涼了。

在之前的面試中,關于閉包總是有種莫名的恐懼,想趕快結束這個話題,進入下一環節,有沒有?我原本想是深入學習一下閉包就好了,但經過我多方考查學習,發現閉包牽涉的知識點是很廣的,需要明白JS引擎的工作機制和一些底層的原理,了解了相關知識點之後,在回過頭了解閉包就容易多了。

文章的最後,會介紹閉包的概念,形成、實作,和使用,以及對性能和記憶體的影響,其實還是很好了解的,學完這篇文章,至少可以讓你在下一次面試中,侃侃而談5分鐘吧。

進入正式主題

介紹執行上下文和執行上下文棧概念

JS中可執行的代碼一共就分為三種:全局代碼、函數代碼、eval代碼。由于eval一般不會使用,這裡不做讨論。而代碼的執行順序總是與代碼編寫先後順序有所差異,先抛開異步問題,就算是同步代碼,它的執行也與預期的不一緻,這說明代碼在執行前一定發生了某些微妙的變化,JS引擎究竟做了什麼呢?

執行上下文

其實JS代碼在執行前,JS引擎總要做一番準備工作,這裡的“準備工作”,用個更專業一點的說法,就叫做"執行上下文(execution context)",對應上述可執行的代碼,會産生不同的執行上下文

1、全局執行上下文:隻有一個,在用戶端中一般由浏覽器建立,也就是window對象,能通過this直接通路到它。

全局對象window上預定義了大量的方法和屬性,在全局環境的任意處都能直接通路這些屬性方法,同時window對象還是var聲明的全局變量的載體。我們通過var建立的全局對象,都可以通過window直接通路。

2、函數執行上下文:可存在無數個,每當一個函數被調用時都會建立一個函數上下文;需要注意的是,同一個函數被多次調用,都會建立一個新的上下文。

執行上下文棧

那麼接下來問題來了,寫的函數多了去了,如何管理建立的那麼多執行上下文呢? JavaScript 引擎建立了執行上下文棧(Execution context stack,ECS)來管理執行上下文。

簡稱執行棧也叫調用棧,執行棧用于存儲代碼執行期間建立的所有上下文,具有FILO(First In Last Out先進後出)的特性。

JS代碼首次運作,都會先建立一個全局執行上下文并壓入到執行棧中,之後每當有函數被調用,都會建立一個新的函數執行上下文并壓入棧内;由于執行棧FILO的特性,是以可以了解為,JS代碼執行完畢前在執行棧底部永遠有個全局執行上下文

棧中的執行順序為:先進後出

僞代碼模拟分析以下代碼中執行上下文棧的行為

function a() {
  b()
}


function b() {
  c()
}


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

定義一個數組來模拟執行上下文棧的行為: ECStack = [];

當 JavaScript 開始要解釋執行代碼時,最先遇到肯定是全局代碼,是以初始化的時候首先就會向執行上下文棧壓入一個全局執行上下文,用 globalContext 表示它,并且隻有當整個應用程式結束的時候,ECStack 才會被清空,是以程式結束之前, ECStack 最底部永遠有個 globalContext:

ECStack = [
    globalContext
];      

執行一個函數,都會建立一個執行上下文,并且壓入執行上下文棧中的棧頂,當函數執行完畢後,就會将該函數的執行上下文從棧頂彈出。

// 按照執行順序,分别建立對應函數的執行上下文,并且壓入執行上下文棧的棧頂
ECStack.push(functionAContext)    // push a
ECStack.push(functionBContext)    // push b
ECStack.push(functionCContext)    // push c


// 棧執行,首先C函數執行完畢,先進後出,
ECStack.pop()   // 彈出c
ECStack.pop()   // 彈出b
ECStack.pop()   // 彈出a


// javascript接着執行下面的代碼,但是ECStack底層永遠有個globalContext,直到整個應用程式結束的時候,ECStack 才會被清空
// ......
// ......      

代碼模拟實作棧的執行過程

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())    // 3
console.log(stack.pop())    // 2
console.log(stack.pop())    // 1      

通過ES3提出的老概念—了解執行上下文

我在閱讀相關資料時,遇到了一個問題,就是關于執行上下文說法不一,不過大緻可以分為兩種觀點,一個是變量對象,活動對象,詞法作用域,作用域鍊,另一個是詞法環境,變量環境,一番查閱可以确定的是,變量對象與活動對象的概念是ES3提出的老概念,從ES5開始就用詞法環境和變量環境替代了,因為更好解釋。

先大緻講一下變量對象,活動對象,詞法作用域,作用域鍊吧

1、變量對象和活動對象

變量對象是與執行上下文相關的資料作用域,存儲了在上下文中定義的變量和函數聲明。不同執行上下文中的變量對象不同,分别看一下全局上下文中的變量對象和函數上下文中的變量對象。

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

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

  • 全局對象是預定義的對象,作為 JavaScript 的全局函數和全局屬性的占位符。通過使用全局對象,可以通路所有其他所有預定義的對象、函數和屬性。
  • 在頂層 JavaScript 代碼中,可以用關鍵字 this 引用全局對象。因為全局對象是作用域鍊的頭,這意味着所有非限定性的變量和函數名都會作為該對象的屬性來查詢。

2)、函數上下文中的變量對象

在函數上下文中用活動對象(activation object, AO)來表示變量對象(VO)。

活動對象和變量對象其實是一個東西,隻是變量對象是規範上的或者說是引擎實作上的,不可在 JavaScript 環境中通路,隻有進入一個執行上下文中時,這個執行上下文的變量對象才會被激活,是以才叫活動對象,而隻有被激活的變量對象(也就是活動對象)上的各種屬性才能被通路。

換句話說:未進入執行階段之前,變量對象(VO)中的屬性都不能通路!進入執行階段之後,變量對象(VO)轉變為了活動對象(AO),裡面的屬性可以被通路,并開始進行執行階段的操作。它們其實都是同一個對象,隻是處于執行上下文的不同生命周期

但是從嚴格角度來說,AO 實際上是包含了 VO 的。因為除了 VO 之外,AO 還包含函數的 parameters,以及 arguments 這個特殊對象。也就是說 AO 的确是在進入到執行階段的時候被激活,但是激活的除了 VO 之外,還包括函數執行時傳入的參數和 arguments 這個特殊對象。

AO = VO + function parameters + arguments

活動對象是在進入函數上下文時刻被激活,通過函數的 arguments 屬性初始化。

執行上下文的代碼會分成兩個階段進行處理,預解析和執行:

  • 預解析的過程會激活AO對象,解析形參,變量提升及函數聲明等
  • 在代碼執行階段,會從上到下順序執行代碼,根據代碼,修改變量對象的值。

2、詞法作用域

作用域是指代碼中定義變量的區域。其規定了如何查找變量,也就是确定目前執行代碼對變量的通路權限。

JavaScript 采用詞法作用域(lexical scoping),也就是靜态作用域,函數的作用域在函數定義的時候就決定了。

詞法作用域根據源代碼中聲明變量的位置來确定該變量在何處可用。嵌套函數可通路聲明于它們外部作用域的變量

// 詞法作用域
var value = 1;


function foo() {
    console.log(value);
}


function bar() {
    var value = 2;
    foo();
}


bar();    // 1      

分析下執行過程:執行 foo ,先從 foo 内部查找是否有局部變量 value,如果沒有,就根據書寫的位置,查找上一層作用域,也就是 value=1,是以列印 1。

看個例子

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()();      

兩段代碼都會列印:local scope。原因也很簡單,因為JavaScript采用的是詞法作用域,函數的作用域基于函數建立的位置。

雖然兩段代碼執行的結果一樣,但是兩段代碼究竟有什麼不同呢?詞法作用域隻是其中的一小部分,還有一個答案就是:執行上下文棧的變化不一樣。

模拟第一段代碼運作時棧中的變化:

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

模拟第二段代碼運作時棧中的變化:

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

3、作用域鍊

每個函數都有自己的執行上下文環境,當代碼在這個環境中執行時,會建立變量對象的作用域鍊,作用域鍊類似一個對象清單,它保證了變量對象的有序通路。

作用域鍊的最前端是目前代碼執行環境的變量對象,也稱“活躍對象AO”,當查找變量的時候,會先從目前上下文的變量對象中查找,如果找到就停止查找,如果沒有就會繼續向上級作用域(父級執行上下文的變量對象)查找,直到找到全局上下文的變量對象(全局對象)

特别注意:作用域鍊的逐級查找,也會影響到程式的性能,變量作用域鍊越長對性能影響越大,這也是為什麼要盡量避免使用全局變量的一個主要原因。

那麼這個作用域鍊是怎麼形成的呢?

這是因為函數有一個内部屬性 [[scope]]:當函數建立時,會儲存所有父變量對象到其中,可以了解 [[scope]] 就是所有父變量對象的層級鍊,當函數激活時,進入函數上下文,會将目前激活的活動對象添加到作用鍊的最前端。

此時就可以了解,查找變量時首先找自己,沒有再找父親。

下面以一個函數的建立和激活兩個時期來講解作用域鍊是如何建立和變化的。

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

函數建立時,各自的[[scope]]為:

foo.[[scope]] = [
  globalContext.VO
];


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

當函數激活時,進入函數上下文,就會将目前激活的活動對象添加到作用鍊的前端。

這時候目前的執行上下文的作用域鍊為 Scope = [AO].concat([[Scope]]);

以下面代碼為例,結合變量對象和執行上下文棧,來總結一下函數執行上下文中作用域鍊和變量對象的建立過程。

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

執行過程如下(僞代碼):

// 1.chec


kscope 函數被建立,儲存父變量對象到 内部屬性[[scope]] 
checkscope.[[scope]] = [
    globalContext.VO
];


// 2.執行 checkscope 函數,建立 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧
ECStack = [
    checkscopeContext,
    globalContext
];


// 3.checkscope 函數并不立刻執行,開始做準備工作,第一步:複制函數[[scope]]屬性建立作用域鍊
checkscopeContext = {
    Scope: checkscope.[[scope]],
}


// 4.用 arguments 建立活動對象,随後初始化活動對象,加入形參、函數聲明、變量聲明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}


// 5.将活動對象壓入 checkscope 作用域鍊Scope的頂端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}


// 6.準備工作做完,開始執行函數,随着函數的執行,修改 AO 的屬性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}


// 7.查找到 scope2 的值,傳回後函數執行完畢,函數上下文從執行上下文棧中彈出
ECStack = [
    globalContext
];      

4、活學活用 — 案例分析

通過案例分析的形式,串聯上述所有知識點,模拟執行上下文建立執行的過程

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


// 1.執行全局代碼,建立全局執行上下文,全局上下文被壓入執行上下文棧
  ECStack = [
    globalContext
  ];


// 2.全局上下文初始化
  globalContext = {
    VO: [global],
    Scope: [globalContext.VO],
    this: globalContext.VO
  }


// 3.初始化的同時,checkscope 函數被建立,儲存作用域鍊到函數的内部屬性[[scope]]
  checkscope.[[scope]] = [
    globalContext.VO
  ];


// 4.執行 checkscope 函數,建立 checkscope 函數執行上下文,并壓入執行上下文棧
  ECStack = [
    checkscopeContext,
    globalContext
  ];


// 5.checkscope 函數執行上下文初始化:
/**
 * 複制函數 [[scope]] 屬性建立作用域鍊,
 * 用 arguments 建立活動對象,
 * 初始化活動對象,即加入形參、函數聲明、變量聲明,
 * 将活動對象壓入 checkscope 作用域鍊頂端。
 * 同時 f 函數被建立,儲存作用域鍊到 f 函數的内部屬性[[scope]]
 */
  checkscopeContext = {
    AO: {
      arguments: {
          length: 0
      },
      scope: undefined,
      f: reference to function f(){}    // 引用函數
    },
    Scope: [AO, globalContext.VO],
    this: undefined
  }


// 6.執行 f 函數,建立 f 函數執行上下文,f 函數執行上下文被壓入執行上下文棧
  ECStack = [
    fContext,
    checkscopeContext,
    globalContext
  ];


// 7.f 函數執行上下文初始化, 以下跟第 5 步相同:
  /**
  複制函數 [[scope]] 屬性建立作用域鍊
  用 arguments 建立活動對象
  初始化活動對象,即加入形參、函數聲明、變量聲明
  将活動對象壓入 f 作用域鍊頂端
  */
  fContext = {
    AO: {
      arguments: {
          length: 0
      }
    },
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
    this: undefined
  }
// 8.f 函數執行,沿着作用域鍊查找 scope 值,傳回 scope 值


// 9.f 函數執行完畢,f 函數上下文從執行上下文棧中彈出
  ECStack = [
    checkscopeContext,
    globalContext
  ];


// 10.checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出
  ECStack = [
    globalContext
  ];      

通過ES5提出的新概念—了解執行上下文

執行上下文建立分為建立階段與執行階段兩個階段,較為難了解應該是建立階段。

建立階段主要負責三件事:

  • 确定this
  • 建立詞法環境(LexicalEnvironment)
  • 建立變量環境(VariableEnvironment)

1、建立階段

ExecutionContext = {  
    ThisBinding = <this value>,  // 确定this
    LexicalEnvironment = {},     // 建立詞法環境
    VariableEnvironment = {},    // 建立變量環境
};      

1)、确定this

官方稱呼為:This Binding,在全局執行上下文中,this總是指向全局對象,例如浏覽器環境下this指向window對象。

而在函數執行上下文中,this的值取決于函數的調用方式,如果被一個對象調用,那麼this指向這個對象。否則this一般指向全局對象window或者undefined(嚴格模式)。

2)、詞法環境

詞法環境中包含辨別符和變量的映射關系,辨別符表示變量/函數的名稱,變量是對實際對象【包括函數類型對象】或原始值的引用。

詞法環境由環境記錄與外部環境引入記錄兩個部分組成。

  • 環境記錄:用于存儲目前環境中的變量和函數聲明的實際位置;
  • 外部環境引入記錄:用于儲存自身環境可以通路的其它外部環境,有點作用域鍊的意思

那麼全局執行上下文與函數執行上下文,也導緻了詞法環境分為全局詞法環境和函數詞法環境兩種。

  • 全局詞法環境:對外部環境的引入記錄為 null,因為它本身就是最外層環境,除此之外它還記錄了目前環境下的所有屬性、方法位置。
  • 函數詞法環境:包含使用者在函數中定義的所有屬性方法外,還包含一個arguments對象(該對象包含了索引和傳遞給函數的參數之間的映射以及傳遞給函數的參數的長度)。函數詞法環境的外部環境引入可以是全局環境,也可以是其它函數環境,這個根據實際代碼而定。

環境記錄在全局和函數中也不同,全局中的環境記錄叫對象環境記錄,函數中環境記錄叫聲明性環境記錄,下方有展示:

  • 對象環境記錄: 用于定義在全局執行上下文中出現的變量和函數的關聯。全局環境包含對象環境記錄。
  • 聲明性環境記錄 存儲變量、函數和參數。一個函數環境包含聲明性環境記錄。
GlobalExectionContext = {    // 全局環境
  LexicalEnvironment: {      // 全局詞法環境
    EnvironmentRecord: {     // 類型為對象環境記錄
      Type: "Object", 
      // 辨別符綁定在這裡 
    },
    outer: < null >
  }
};


FunctionExectionContext = {   // 函數環境
  LexicalEnvironment: {       // 函數詞法環境
    EnvironmentRecord: {      // 類型為聲明性環境記錄
      Type: "Declarative", 
      // 辨別符綁定在這裡 
    },
    outer: < Global or outerfunction environment reference >
  }
};      

3)、變量環境

它也是一個詞法環境,它具備詞法環境所有屬性,同樣有環境記錄與外部環境引入。

在ES6中唯一的差別在于詞法環境用于存儲函數聲明與let const聲明的變量,而變量環境僅僅存儲var聲明的變量。

let a = 20;  
const b = 30;  
var c;


function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}


c = multiply(20, 30);      

上述代碼中執行上下文的建立過程:

//全局執行上下文
GlobalExectionContext = {
    ThisBinding: Global Object,  // this綁定為全局對象
    LexicalEnvironment: {          // 詞法環境
      EnvironmentRecord: {         
        Type: "Object",            // 對象環境記錄
        // let const建立的變量a b在這
        a:  uninitialized ,  
        b:  uninitialized ,  
        multiply: < func >  
      }
      outer: null                // 全局環境外部環境引入為null
    },


    VariableEnvironment: {         // 變量環境
      EnvironmentRecord: {         
        Type: "Object",            // 對象環境記錄
        // var建立的c在這
        c: undefined,  
      }
      outer: null                // 全局環境外部環境引入為null
    }  
  }


// 函數執行上下文
FunctionExectionContext = {
  ThisBinding: Global Object, //由于函數是預設調用 this綁定同樣是全局對象
  LexicalEnvironment: {          // 詞法環境
    EnvironmentRecord: {         
      Type: "Declarative",       // 聲明性環境記錄
      // arguments對象在這
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: GlobalEnvironment    // 外部環境引入記錄為Global
  },


  VariableEnvironment: {          // 變量環境
    EnvironmentRecord: {          
      Type: "Declarative",        // 聲明性環境記錄
      // var建立的g在這
      g: undefined  
    },  
    outer: GlobalEnvironment    // 外部環境引入記錄為Global
  }  
}      

這會引發我們另外一個思考,那就是變量提升的原因:

我們會發現在建立階段,代碼會被掃描并解析變量和函數聲明,其中let 和 const 定義的變量沒有任何與之關聯的值,會保持未初始化的狀态。

但 var 定義的變量設定為 undefined。

是以這就是為什麼可以在聲明之前,通路到 var 聲明的變量(盡管是 undefined),但如果在聲明之前通路 let 和 const 聲明的變量就會報錯的原因,也就是常說的暫時性死區,

在執行上下文建立階段,函數聲明與var聲明的變量在建立階段已經被賦予了一個值,var聲明被設定為了undefined,函數被設定為了自身函數,而let const被設定為未初始化。這是因為執行上下文建立階段JS引擎對兩者初始化指派不同。

執行階段

上下文除了建立階段外,還有執行階段,代碼執行時根據之前的環境記錄對應指派,比如早期var在建立階段為undefined,如果有值就對應指派,像let const值為未初始化,如果有值就指派,無值則賦予undefined。

執行上下文總結

  1. 全局執行上下文一般由浏覽器建立,代碼執行時就會建立;函數執行上下文隻有函數被調用時才會建立,同一個函數被多次調用,都會建立一個新的上下文。
  2. 調用棧用于存放所有執行上下文,滿足FILO特性。
  3. 執行上下文建立階段分為綁定this,建立詞法環境,變量環境三步,兩者差別在于詞法環境存放函數聲明與const let聲明的變量,而變量環境隻存儲var聲明的變量。
  4. 詞法環境主要由環境記錄與外部環境引入記錄兩個部分組成,全局上下文與函數上下文的外部環境引入記錄不一樣,全局為null,函數為全局環境或者其它函數環境。環境記錄也不一樣,全局叫對象環境記錄,函數叫聲明性環境記錄。
  5. ES3之前的變量對象與活動對象的概念在ES5之後由詞法環境,變量環境來解釋,兩者概念不沖突,後者了解更為通俗易懂。

閉包

上文說了這麼多,其實我本意隻是想聊一聊閉包的,終于回歸正題。

閉包是什麼?

MDN 對閉包的定義簡單了解就是:

閉包是由函數以及聲明該函數的詞法環境組合而成的。該環境包含了這個閉包建立時作用域内的任何局部變量(閉包維持了一個對它的詞法環境的引用:在一個函數内部定義的函數,會将外部函數的活躍對象添加到自己的作用域鍊中)。

是以可以在一個内層函數中通路到其外層函數的作用域。在 JavaScript 中,每當建立一個函數,閉包就會在函數建立的同時被建立出來。

人們常說的閉包無非就是:函數内部傳回一個函數,一是可以讀取并操作函數内部的變量,二是可以讓這些變量的值始終儲存在記憶體中。

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

  1. 從理論角度:所有的函數。因為它們都在建立時儲存了上層上下文的資料。哪怕是簡單的全局變量也是如此,因為函數中通路全局變量就相當于是在通路自由變量,這個時候使用最外層的作用域。
  2. 從實踐角度:閉包無非滿足以下兩點:
  • 閉包首先得是一個函數。
  • 閉包能通路外部函數作用域中的自由變量,即使外部函數上下文已銷毀。(也可以了解為是自帶了執行環境的函數)

閉包的形成與實作

上文中介紹過JavaScript是采用詞法作用域的,講的是函數的執行依賴于函數定義的時候所産生的變量作用域。

為了去實作這種詞法作用域,JavaScript函數對象的内部狀态不僅包含函數邏輯的代碼,還包含目前作用域鍊的引用。

函數對象可以通過這個作用域鍊互相關聯起來,函數體内部的變量都可以儲存在函數的作用域内

let scope = "global scope";
function checkscope() {
    let scope = "local scope";   // 自由變量
    function f() {    // 閉包
        console.log(scope);
    };
    return f;
};


let foo = checkscope();
foo();      
// 1. 僞代碼分别表示執行棧中上下文的變化,以及上下文建立的過程,首先執行棧中永遠都會存在一個全局執行上下文。
ECStack = [GlobalExecutionContext];


// 2. 此時全局上下文中存在兩個變量scope、foo與一個函數checkscope,上下文用僞代碼表示具體是這樣:
GlobalExecutionContext = {     // 全局執行上下文
    ThisBinding: Global Object  ,
    LexicalEnvironment: {      // 詞法環境
        EnvironmentRecord: {
            Type: "Object",    // 對象環境記錄
            scope: uninitialized ,
            foo: uninitialized ,
            checkscope: func 
        }
        outer: null   // 全局環境外部環境引入為null
    }
}
// 3. 全局上下文建立階段結束,進入執行階段,全局執行上下文的辨別符中像scope、foo之類的變量被指派,然後開始執行checkscope函數,于是一個新的函數執行上下文被建立,并壓入執行棧中:
ECStack = [checkscopeExecutionContext,GlobalExecutionContext];


// 4. checkscope函數執行上下文進入建立階段:
checkscopeExecutionContext = {      // 函數執行上下文
    ThisBinding: Global Object,
    LexicalEnvironment: {           // 詞法環境
        EnvironmentRecord: {
            Type: "Declarative",    // 聲明性環境記錄
            Arguments: {},
            scope: uninitialized ,
            f: func 
        },
        outer: GlobalLexicalEnvironment   // 外部環境引入記錄為<Global>
    }
}


// 5. checkscope() 等同于window.checkscope() ,是以checkExectionContext 中this指向全局,而且外部環境引用outer也指向了全局(作用域鍊),其次在辨別符中記錄了arguments對象以及變量scope與函數f
// 6. 函數 checkscope 執行到傳回函數 f 時,函數執行完畢,checkscope 的執行上下文被彈出執行棧,是以此時執行棧中又隻剩下全局執行上下文:
ECStack = [GlobalExecutionContext];


// 7. 代碼foo()執行,建立foo的執行上下文,
ECStack = [fooExecutionContext, GlobalExecutionContext];


// 8. foo的執行上下文是這樣:
fooExecutionContext = {
    ThisBinding: Global Object ,
    LexicalEnvironment: {             // 詞法環境
        EnvironmentRecord: {
            Type: "Declarative",      // 聲明性環境記錄
            Arguments: {},
        },
        outer: checkscopeEnvironment  // 外部環境引入記錄為<checkscope>
    }
}
// 9. foo()等同于window.foo(),是以this指向全局window,但outer外部環境引入有點不同,指向了外層函數 checkscope(原因是JS采用詞法作用域,也就是靜态作用域,函數的作用域在定義時就确定了,而不是執行時确定)
/**
 * a. 但是可以發現的是,現在執行棧中隻有 fooExecutionContext 和 GlobalExecutionContext, checkscopeExecutionContext 在執行完後就被釋放了,怎麼還能通路到 其中的變量?
 * b. 正常來說确實是不可以,但是因為閉包 foo 外部環境 outer 的引用,進而讓 checkscope作用域中的變量依舊存活在記憶體中,無法被釋放,是以有時有必要手動釋放自由變量。
 * c. 總結一句,閉包是指能使用其它作用域自由變量的函數,即使作用域已銷毀。
 */      

閉包有什麼用?

說閉包聊閉包,結果閉包有啥用都不知道,甚至遇到了一個閉包第一時間都沒反應過來這是閉包,說說閉包有啥用:

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

所謂私有屬性方法其實就是這些屬性方法隻能被同一個類中的其它方法所調用,但是JavaScript中并未提供專門用于建立私有屬性的方法,但可以通過閉包模拟它:

私有方法不僅有利于限制對代碼的通路:還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。

例一:通過自執行函數傳回了一個對象,隻建立了一個詞法環境,為三個閉包函數所共享:fn.increment,fn.decrement 和 fn.value,除了這三個方法能通路到變量privateCounter和 changeBy函數外,無法再通過其它手段操作它們。

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();     // 0
fn.increment();
fn.increment();
fn.value();     // 2
fn.decrement();
fn.value();     // 1      

例二:構造函數中也有閉包:

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

若某個屬性方法在所有執行個體中都需要使用,一般都會推薦加在構造函數的原型上,還有種做法就是利用私有屬性。

比如這個例子中所有執行個體都可以正常使用變量 age,将age稱為私有屬性的同時,也會将this.hello稱為特權方法,因為隻有通過這個方法才能通路被保護的私有屬性age。

2)、工廠函數

使用函數工廠建立了兩個新函數 — 一個将其參數和 5 求和,另一個和 10 求和。 add5 和 add10 都是閉包。

它們共享相同的函數定義,但是儲存了不同的詞法環境。在 add5 的環境中,x 為 5。在 add10 中,x 則為 10。

利用了閉包自帶執行環境的特性(即使外層作用域已銷毀),僅僅使用一個形參完成了兩個形參求和。

當然例子函數還有個更專業的名詞,叫函數柯裡化。

function makeAdder(x) {
    return function (y) {
        console.log(x + y);
    };
};


var add5 = makeAdder(5);
var add10 = makeAdder(10);
add5(2); // 7
add10(2); // 12      

閉包對性能和記憶體的影響

  1. 閉包會額外附帶函數的作用域(内部匿名函數攜帶外部函數的作用域),會比其它函數多占用些記憶體空間,過度的使用可能會導緻記憶體占用的增加。
  2. 閉包中包含與函數執行上下文相同的作用域鍊引用,是以會産生一定的負面作用,當函數中活躍對象和執行上下文銷毀時,由于閉包仍存在對活躍對象的引用,導緻活躍對象無法銷毀,可能會導緻記憶體洩漏。
  3. 閉包中如果存在對外部變量的通路,會增加辨別符的查找路徑,在一定的情況下,也會造成性能方面的損失。解決此類問題的辦法:盡量将外部變量存入到局部變量中,減少作用域鍊的查找長度。

綜上所述:如果不是某些特定任務需要使用閉包,在其它函數中建立函數是不明智的,因為在處理速度和記憶體消耗方面對腳本性能具有負面影響。

繼續閱讀