官方 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 的編譯過程來更加直觀的解釋。面試
大體分爲三個步驟:算法
var a = 1;
,會被分紅 var
、 a
、 1
、 ;
這樣的原子符號((atomic token)。詞法分析=指登記變量聲明+函數聲明+函數聲明的形參。var a = 1; console.log(a); a = ; // Uncaught SyntaxError: Unexpected token ; // 代碼並無打印出來 1 ,而是直接報錯,說明在代碼執行前進行了詞法分析、語法分析
在第一步中,咱們看到有詞法分析,它用來登記變量聲明、函數聲明以及函數聲明的形參,後續代碼執行的時候就能夠知道要從哪裏去獲取變量值與函數。這個登記的地方就是詞法環境。數組
詞法環境包含兩部分:閉包
每一個環境能訪問到的標識符集合,咱們稱之爲「做用域」。咱們將做用域一層一層嵌套,造成了「做用域鏈」。函數
詞法環境有兩種 類型 :this
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 採用的是詞法做用域(靜態做用域),即它在代碼寫好以後就被靜態決定了它的做用域。
動態做用域是基於棧結構,局部變量與函數參數都存儲在棧中,因此,變量的值是由代碼運行時當前棧的棧頂執行上下文決定的。而靜態做用域是指變量建立時就決定了它的值,源代碼的位置決定了變量的值。
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
面試題庫
算法題庫