瞭解詞法環境嗎?它和閉包有什麼聯繫?

詞法環境(Lexical Environment)

官方定義

官方 ES2020 這樣定義詞法環境(Lexical Environment):前端

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.

詞法環境是一種規範類型(specification type),它基於 ECMAScript 代碼的詞法嵌套結構,來定義標識符與特定變量和函數的關聯關係。詞法環境由環境記錄(environment record)和可能爲空引用(null)的外部詞法環境組成。git

說的很詳細,但是很難理解喃🤔github

下面,咱們經過一個 V8 中 JS 的編譯過程來更加直觀的解釋。面試

V8 中 JS 的編譯過程來更加直觀的解釋

大體分爲三個步驟:算法

  • 第一步 詞法分析 :V8 剛拿到執行上下文的時候,會把代碼從上到下一行一行的進行分詞/詞法分析(Tokenizing/Lexing),例如 var a = 1; ,會被分紅 vara1; 這樣的原子符號((atomic token)。詞法分析=指登記變量聲明+函數聲明+函數聲明的形參。
  • 第二步 語法分析 :在詞法分析結束後,會作語法分析,引擎將 token 解析成一個抽象語法樹(AST),在這一步會檢測是否有語法錯誤,若是有則直接報錯再也不往下執行
var a = 1;
console.log(a);
a = ;
// Uncaught SyntaxError: Unexpected token ;
// 代碼並無打印出來 1 ,而是直接報錯,說明在代碼執行前進行了詞法分析、語法分析
  • 注意: 詞法分析跟語法分析不是徹底獨立的,而是交錯運行的。也就是說,並非等全部的 token 都生成以後,才用語法分析器來處理。通常都是每取得一個 token ,就開始用語法分析器來處理了
  • 第三步 代碼生成 :最後一步就是將 AST 轉成計算機能夠識別的機器指令碼

在第一步中,咱們看到有詞法分析,它用來登記變量聲明、函數聲明以及函數聲明的形參,後續代碼執行的時候就能夠知道要從哪裏去獲取變量值與函數。這個登記的地方就是詞法環境。數組

詞法環境包含兩部分:閉包

  • 環境記錄:存儲變量和函數聲明的實際位置,真正用來登記變量的地方
  • 對外部環境的引用:意味着它能夠訪問其外部詞法環境,是做用域鏈可以鏈接起來的關鍵

每一個環境能訪問到的標識符集合,咱們稱之爲「做用域」。咱們將做用域一層一層嵌套,造成了「做用域鏈」。函數

詞法環境有兩種 類型 :this

  • 全局環境:是一個沒有外部環境的詞法環境,其外部環境引用爲 null。擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,this 的值指向這個全局對象。
  • 函數環境:用戶在函數中定義的變量被存儲在環境記錄中,包含了arguments 對象。對外部環境的引用能夠是全局環境,也能夠是包含內部函數的外部函數環境。

環境記錄 一樣有兩種類型:atom

  • 聲明性環境記錄 :存儲變量、函數和參數。一個函數環境包含聲明性環境記錄。
  • 對象環境記錄 :用於定義在全局執行上下文中出現的變量和函數的關聯。全局環境包含對象環境記錄。

若是用僞代碼的形式表示,詞法環境是這樣噠:

GlobalExectionContext = {  // 全局執行上下文
  LexicalEnvironment: {          // 詞法環境
    EnvironmentRecord: {           // 環境記錄
      Type: "Object",                 // 全局環境
      // ...
      // 標識符綁定在這裏 
    },
    outer: <null>                    // 對外部環境的引用
  }  
}

FunctionExectionContext = { // 函數執行上下文
  LexicalEnvironment: {        // 詞法環境
    EnvironmentRecord: {          // 環境記錄
      Type: "Declarative",         // 函數環境
      // ...
      // 標識符綁定在這裏               // 對外部環境的引用
    },
    outer: <Global or outer function environment reference>  
  }  
}

例如:

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>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 標識符綁定在這裏  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 標識符綁定在這裏  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  
   
  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 標識符綁定在這裏  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 標識符綁定在這裏  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

詞法環境與咱們本身寫的代碼結構相對應,也就是咱們本身代碼寫成什麼樣子,詞法環境就是什麼樣子。詞法環境是在代碼定義的時候決定的,跟代碼在哪裏調用沒有關係。因此說 JS 採用的是詞法做用域(靜態做用域),即它在代碼寫好以後就被靜態決定了它的做用域。

靜態做用域 vs 動態做用域

動態做用域是基於棧結構,局部變量與函數參數都存儲在棧中,因此,變量的值是由代碼運行時當前棧的棧頂執行上下文決定的。而靜態做用域是指變量建立時就決定了它的值,源代碼的位置決定了變量的值。

var x = 1;

function foo() {
  var y = x + 1;
  return y;
}

function bar() {
  var x = 2;
  return foo();
}

foo(); // 靜態做用域: 2; 動態做用域: 2
bar(); // 靜態做用域: 2; 動態做用域: 3

在此例中,靜態做用域與動態做用域的執行結構多是不一致的,bar 本質上就是執行 foo 函數,若是是靜態做用域的話, bar 函數中的變量 x 是在 foo 函數建立的時候就肯定了,也就是說變量 x 一直爲 1 ,兩次輸出應該都是 2 。而動態做用域則根據運行時的 x 值而返回不一樣的結果。

因此說,動態做用域常常會帶來不肯定性,它不能肯定變量的值究竟是來自哪一個做用域的。

大多數如今程序設計語言都是採用靜態做用域規則,如C/C++、C#、Python、Java、JavaScript等,採用動態做用域的語言有Emacs Lisp、Common Lisp(兼有靜態做用域)、Perl(兼有靜態做用域)。C/C++的宏中用到的名字,也是動態做用域。

詞法環境與閉包

一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一塊兒(或者說函數被引用包圍),這樣的組合就是閉包closure

——MDN

也就是說,閉包是由 函數 以及聲明該函數的 詞法環境 組合而成的

var x = 1;

function foo() {
  var y = 2; // 自由變量
  function bar() {
    var z = 3; //自由變量
    return x + y + z;
  }
  return bar;
}

var test = foo();

test(); // 6

基於咱們對詞法環境的理解,上述例子能夠抽象爲以下僞代碼:

GlobalEnvironment = {
  EnvironmentRecord: { 
    // 內置標識符
    Array: '<func>',
    Object: '<func>',
    // 等等..

    // 自定義標識符
    x: 1
  },
  outer: null
};

fooEnvironment = {
  EnvironmentRecord: {
    y: 2,
    bar: '<func>'
  }
  outer: GlobalEnvironment
};

barEnvironment = {
  EnvironmentRecord: {
    z: 3
  }
  outer: fooEnvironment
};

前面說過,詞法做用域也叫靜態做用域,變量在詞法階段肯定,也就是定義時肯定。雖然在 bar 內調用,但因爲 foo 是閉包函數,即便它在本身定義的詞法做用域之外的地方執行,它也一直保持着本身的做用域。所謂閉包函數,即這個函數封閉了它本身的定義時的環境,造成了一個閉包,因此 foo 並不會從 bar 中尋找變量,這就是靜態做用域的特色。

爲了實現閉包,咱們不能用動態做用域的動態堆棧來存儲變量。若是是這樣,當函數返回時,變量就必須出棧,而再也不存在,這與最初閉包的定義是矛盾的。事實上,外部環境的閉包數據被存在了「堆」中,這樣才使得即便函數返回以後內部的變量仍然一直存在(即便它的執行上下文也已經出棧)。

最後

本文首發自「三分鐘學前端」,天天三分鐘,進階一個前端小 tip

面試題庫
算法題庫
相關文章
相關標籤/搜索