深入理解閉包之前置知識→作用域與詞法作用域

lce_shou發表於2018-05-15

前言

這兩天剛好和朋友討論到閉包,這個JavaScript中的“神獸”,很多同學會覺得閉包這玩意太鬧心了,怎麼著都理解不了...其實剛接觸JavaScript的時候我也是這樣的。

但是呢,閉包卻非常重要!非常重要!非常重要! 在《你不知道的JavaScript》中甚至這樣寫道“對於那些有一點 JavaScript 使用經驗但從未真正理解閉包概念的人來說,理解閉包可以看作是某種意義上的重生”。

所以看到這裡,各位親是不是迫切的想要深入的去了解一下閉包了呢? 不急,不急,對於真正的理解閉包有一個非常重要的前置知識,那就是作用域與詞法作用域,如果你沒能好好理解詞法作用域,那麼閉包是肯定理解不了的!那麼接下來就好好的理解一下詞法作用域吧。

作用域

我們先丟擲一個概念:“詞法作用域是作用域的一種工作模型”,先不管這句話的深層次的意思,就但看表面,我們就應該可以得出一個結論,那就是沒有作用域的概念就沒有詞法作用域的概念。所以...接下來,你懂的...

什麼是作用域

一言以蔽之,“作用域就是一套規則,用於確定在何處以及如何查詢變數(識別符號)的規則”。在這句話中讀到一個關鍵點 查詢變數(識別符號),那麼就從查詢變數說起吧。

先看一段及其簡單的程式碼

function foo() {
	var a = 'iceman';
	console.log(a); // 輸出"iceman"
}
foo();
複製程式碼

在foo函式執行的時候,輸出一個a變數,那麼這個a變數是哪裡來的嘞,有看到函式第一行有定義a變數的程式碼var a = 'iceman'

再看一段同樣簡單的程式碼

var b = 'programmer';
function foo() {
	console.log(b); // 輸出"programmer"
}
foo();
複製程式碼

同樣的道理,在輸出b的時候,自己函式內部沒有找到變數b,那麼就在外層的全域性中查詢,找到了就停止查詢並輸出了。

注意以上兩段程式碼都有查詢變數,第一段程式碼是在函式中找到a變數,第二段程式碼是在全域性中找到b變數。現在閉上眼睛,我要給加粗的這兩個詞的後面加上幾個字了!

好了,開啟眼睛,Duang,Duang --->函式作用域全域性作用域,把這兩個詞換入到原來那句話中,第一段程式碼是在函式作用域中找到a變數,第二段程式碼是在全域性作用域中找到b變數。

所以,懂了沒有呢?通俗的講,作用域就是查詢變數的地方。在某函式中找到該變數,就可以說在該函式作用域中找到了該變數;在全域性中找到該變數,就可以說在全域性作用域中找到了該變數!

不知道各位同學有沒注意到一個細節,我們在查詢b變數的時候,先在函式作用域中查詢,沒有找到,再去全域性作用域中查詢,有一個往外層查詢的過程。我們好像是順著一條鏈條從下往上查詢變數,這條鏈條,我們就稱之為作用域鏈

作用域巢狀

在還沒有接觸到ES6的let、const之前,只有函式作用域和全域性作用域,函式作用域肯定是在全域性作用域裡面的,而函式作用域中又可以繼續巢狀函式作用域,如圖:

作用域巢狀.png

用程式碼表示:

作用域巢狀.png

以上兩張圖可以很直觀的看出作用域的巢狀關係了吧。查詢變數也是順著紅色的箭頭走的,從裡到外,這從裡到外的各層作用域就組成了作用域鏈。

作用域中變數(識別符號)的查詢規則

首先宣告一點,JavaScript是有編譯過程的,不要驚訝,真的有!也就是說var name = 'iceman'這段程式碼,其實這是有兩個動作的:

  • 編譯器在當前作用域中宣告一個變數name

  • 執行時引擎在作用域中查詢該變數,找到了name變數併為其賦值

證明以上的說法:

console.log(name); // 輸出undefined
var name = 'iceman'; 
複製程式碼

var name = 'iceman'的上一行輸出name變數,並沒有報錯,輸出undefined,說明輸出的時候該變數已經存在了,只是沒有賦值而已。

其實編譯器是這樣工作的,在程式碼執行之前從上到下的進行編譯,當遇到某個用var宣告的變數的時候,先檢查在當前作用域下是否存在了該變數。如果存在,則忽略這個宣告;如果不存在,則在當前作用域中宣告該變數。

上面的這段簡單的程式碼包含兩種查詢型別:輸出變數的值的時候的查詢型別是RHS,找到變數為其賦值的查詢型別是LHS。

我猜各位同學一定可以猜到“L”和“R”的含義,這裡的左側和右側指的是在賦值操作的左側和右側。也就是說,變數出現在賦值操作的左側時進行LHS查詢,出現在右側時進行RHS查詢。

用一句通俗的話來講,RHS就是取到它的源值。

注意:“賦值操作的左側和右側”,並不意味著只是“=”,實際上賦值操作還有好幾種形式。

在作用域中查詢變數都是RHS,並且查詢的規則是從當前作用域開始找,如果沒找到再到父級作用域中找,一層層往外找,如果在全域性作用域如果還沒找到的話,就會報錯了:ReferenceError: 某變數 is not defined

所有的賦值操作中查詢變數都是LHS。其中a=4這類賦值操作,也是會從當前作用域中查詢,如果沒有找到再到外層作用域中找,如果到全域性變數啊這個變數,在非嚴格模式下會建立一個全域性變數a。不過,非常不建議這麼做,因為輕則汙染全域性變數,重則造成記憶體洩漏(比如:a = 一個非常大的陣列,a在全域性變數中,一直用有引用,程式不會自動將其銷燬)。

詞法作用域

在上面的作用域介紹中,我們將作用域定義為一套規則,這套規則來管理瀏覽器引擎如何在當前作用域以及巢狀的作用域中根據變數(識別符號)進行變數查詢。

我們在前面有丟擲一個概念:“詞法作用域是作用域的一種工作模型”,作用域有兩種工作模型,在JavaScript中的詞法作用域是比較主流的一種,另一種動態作用域(比較少的語言在用)。

所謂的詞法作用域就是在你寫程式碼時將變數和塊作用域寫在哪裡來決定,也就是詞法作用域是靜態的作用域,在你書寫程式碼時就確定了

請看以下程式碼:

function fn1(x) {
	var y = x + 4;
	function fn2(z) {
		console.log(x, y, z);
	}
	fn2(y * 5);
}
fn1(6); // 6 10 50
複製程式碼

這個例子中有個三個巢狀的作用域,如圖:

image.png

  • A 為全域性作用域,有一個識別符號:fn1

  • B 為fn1所建立的作用域,有三個識別符號:x、y、fn2

  • C為fn2所建立的作用域,有一個識別符號:z

作用域是由期程式碼寫在哪裡決定的,並且是逐級包含的。

在此強調,詞法作用域就是作用域是由書寫程式碼時函式宣告的位置來決定的。編譯階段就能夠知道全部識別符號在哪裡以及是如何宣告的,所以詞法作用域是靜態的作用域,也就是詞法作用域能夠預測在執行程式碼的過程中如何查詢識別符號。

注1:eval()和with可以通過其特殊性用來“欺騙”詞法作用域,不過正常情況下都不建議使用,會產生效能問題。

注2:ES6中有了let、const就有了塊級作用域,後面會專門介紹。

特別注意

可以關注我的公眾號:icemanFE,接下來持續更新技術文章!

公眾號.png

相關文章