作用域、作用域鏈及閉包(一)

Escape_Master發表於2019-04-01

正文

作用域是什麼

作用域是一套規則,用於確定在何處以及如何查詢變數。

瞭解作用域之前還要簡單瞭解一下編譯原理: 書中(你不知道的js)說道JavaScript是一門編譯語言。在傳統編譯語言的流程中,程式中一段原始碼在執行之前會經歷三個步驟,統稱為“編譯”。

  • 分詞/詞法分析將字串分解成有意義的程式碼塊,程式碼塊又稱詞法單元。
  • 解析/語法分析將詞法單元流轉換成一個由元素逐級巢狀所組成的代表了程式語法介面的數,又稱“抽象語法樹”。
  • 程式碼生成:將抽象語法樹轉換為機器能夠識別的指令。

理解作用域

作用域與編譯器、引擎進行配合完成程式碼的解析

書中舉了個例子 對於 var a = 2 這條語句,首先編譯器會將其分為兩部分,一部分是 var a,一部分是 a = 2。編譯器會在編譯期間執行 var a,然後到作用域中去查詢 a 變數,如果 a 變數在作用域中還沒有宣告,那麼就在作用域中宣告 a 變數,如果 a 變數已經存在,那就忽略 var a 語句。然後編譯器會為 a = 2 這條語句生成執行程式碼,以供引擎執行該賦值操作。所以我們平時所提到的變數提升,其實就是利用這個先宣告後賦值的原理。

作用域的工作模式

作用域共有兩種主要的工作模型。第一種是最為普遍的,被大多數程式語言所採用的詞法作用域( JavaScript中的作用域就是詞法作用域)。另外一種是動態作用域,仍有一些程式語言在使用(比如Bash指令碼、Perl中的一些模式等)。

異常情況

對於 var a = 10 這條賦值語句,實際上是為了查詢變數 a, 並且將 10 這個數值賦予它,這就是 LHS 查詢。 對於 console.log(a) 這條語句,實際上是為了查詢 a 的值並將其列印出來,這是 RHS 查詢。

為什麼區分 LHS 和 RHS 是一件重要的事情?

在非嚴格模式下,LHS 呼叫查詢不到變數時會建立一個全域性變數,RHS 查詢不到變數時會丟擲 ReferenceError。 在嚴格模式下,LHS 和 RHS 查詢不到變數時都會丟擲 ReferenceError。

詞法作用域

詞法作用域是一套關於引擎如何尋找變數以及會在何處找到變數的規則。詞法作用域最重要的特徵是它的定義過程發生在程式碼的書寫階段(假設沒有使用 eval() 或 with )

舉個?

function testA() {
  console.log(a);  // 2
}

function testB() {
  var a = 3;
  testA();
}

var a = 2;

testB()
詞法作用域讓testA()中的a通過RHS引用到了全域性作用域中的a,因此會輸出2。
複製程式碼

動態作用域

動態作用域只關心它們從何處呼叫。換句話說,作用域鏈是基於呼叫棧的,而不是程式碼中的作用域巢狀。

// 因此,如果 JavaScript 具有動態作用域,理論上,下面程式碼中的 testA() 在執行時將會輸出3。
function testA() {
  console.log(a);  // 3
}

function testB() {
  var a = 3;
  testA();
}

var a = 2;

testB()
複製程式碼

函式作用域

具名與匿名

書中舉了個例子->回撥函式

setTimeout( function() {
  console.log("我等了好久!")
}, 1000 )
複製程式碼

其實這個叫函式匿名錶達式,函式表示式可以匿名,而函式宣告則不可以省略函式名。匿名函式表示式書寫起來簡單快捷,很多庫和工具也傾向鼓勵使用這種風格的程式碼。但它也有幾個缺點需要考慮。

  • 匿名函式在棧追蹤中不會顯示出有意義的函式名,使得除錯很困難。
  • 如果沒有函式名,當函式需要引用自身時只能使用已經過期的 arguments.callee 引用,比如在遞迴中。另一個函式需要引用自身的例子,是在事件觸發後事件監聽器需要解綁自身。
  • 匿名函式省略了對於程式碼可讀性/可理解性很重要的函式名。一個描述性的名稱可以讓程式碼不言自明。

針對於這種缺點,書中給出了建議:給函式表示式命名

setTimeout( function timeoutHandler() {
  console.log("我等了好久!")
}, 1000 )
複製程式碼

提升

先提出個問題,現有賦值還是先有宣告

a = 2;

var a;

console.log(a); // 2
複製程式碼

等價於

var scope="global";
function scopeTest(){
    var scope;
    console.log(scope);
    scope="local"  
}
scopeTest(); //undefined
複製程式碼

我們習慣將 var a = 2; 看作一個宣告,而實際上 JavaScript 引擎並不這麼認為。它將 var a 和 a = 2 當作兩個單獨的宣告,第一個是編譯階段的任務,而第二個是執行階段的任務。

這意味著無論作用域中的宣告出現在什麼地方,都將在程式碼本身被執行前首先進行處理。可以將這個過程形象地想象成所有的宣告(變數和函式)都會被“移動”到各自作用域的最頂端,這個過程稱為提升。

所以可以看出先有宣告後有賦值

再看個小?

foo();  // TypeError
bar();  // ReferenceError

var foo = function bar() {
  // ...
};
複製程式碼

這個程式碼片段經過提升後,實際上會被理解為以下形式:

var foo;
foo();  // TypeError
bar();  // ReferenceError

foo = function() {
  var bar = ...self...
  // ...
};
複製程式碼

這段程式中的變數識別符號 foo() 被提升並分配給全域性作用域,因此 foo() 不會導致 ReferenceError。但是foo此時並沒有賦值(如果它是一個函式宣告而不是函式表示式就會賦值)。foo()由於對 undefined 值進行函式呼叫而導致非法操作,因此丟擲 TypeError 異常。另外即時是具名的函式表示式,名稱識別符號(這裡是 bar )在賦值之前也無法在所在作用域中使用。

結語

希望大家都能找到適合自己的學習方法重學前端,完善自己的知識體系架構~

相關文章