這兩天恰好和朋友討論到閉包,這個JavaScript中的「神獸」,不少同窗會以爲閉包這玩意太鬧心了,怎麼着都理解不了...其實剛接觸JavaScript的時候我也是這樣的。javascript
可是呢,閉包卻很是重要!很是重要!很是重要! 在《你不知道的JavaScript》中甚至這樣寫道「對於那些有一點 JavaScript 使用經驗但從未真正理解閉包概念的人來講,理解閉包能夠看做是某種意義上的重生」。java
因此看到這裏,各位親是否是迫切的想要深刻的去了解一下閉包了呢? 不急,不急,對於真正的理解閉包有一個很是重要的前置知識,那就是做用域與詞法做用域,若是你沒能好好理解詞法做用域,那麼閉包是確定理解不了的!那麼接下來就好好的理解一下詞法做用域吧。數組
咱們先拋出一個概念:「詞法做用域是做用域的一種工做模型」,先無論這句話的深層次的意思,就但看表面,咱們就應該能夠得出一個結論,那就是沒有做用域的概念就沒有詞法做用域的概念。因此...接下來,你懂的...瀏覽器
一言以蔽之,「做用域就是一套規則,用於肯定在何處以及如何查找變量(標識符)的規則」。在這句話中讀到一個關鍵點 查找變量(標識符),那麼就從查找變量提及吧。閉包
先看一段及其簡單的代碼函數
function foo() {
var a = 'iceman';
console.log(a); // 輸出"iceman"
}
foo();
複製代碼
在foo函數執行的時候,輸出一個a變量,那麼這個a變量是哪裏來的嘞,有看到函數第一行有定義a變量的代碼var a = 'iceman'
。性能
再看一段一樣簡單的代碼ui
var b = 'programmer';
function foo() {
console.log(b); // 輸出"programmer"
}
foo();
複製代碼
一樣的道理,在輸出b的時候,本身函數內部沒有找到變量b,那麼就在外層的全局中查找,找到了就中止查找並輸出了。spa
注意以上兩段代碼都有查找變量,第一段代碼是在函數中找到a變量,第二段代碼是在全局中找到b變量。如今閉上眼睛,我要給加粗的這兩個詞的後面加上幾個字了!3d
好了,打開眼睛,Duang,Duang --->函數做用域、全局做用域,把這兩個詞換入到原來那句話中,第一段代碼是在函數做用域中找到a變量,第二段代碼是在全局做用域中找到b變量。
因此,懂了沒有呢?通俗的講,做用域就是查找變量的地方。在某函數中找到該變量,就能夠說在該函數做用域中找到了該變量;在全局中找到該變量,就能夠說在全局做用域中找到了該變量!
不知道各位同窗有沒注意到一個細節,咱們在查找b變量的時候,先在函數做用域中查找,沒有找到,再去全局做用域中查找,有一個往外層查找的過程。咱們好像是順着一條鏈條從下往上查找變量,這條鏈條,咱們就稱之爲做用域鏈。
在尚未接觸到ES6的let、const以前,只有函數做用域和全局做用域,函數做用域確定是在全局做用域裏面的,而函數做用域中又能夠繼續嵌套函數做用域,如圖:
用代碼表示:
以上兩張圖能夠很直觀的看出做用域的嵌套關係了吧。查找變量也是順着紅色的箭頭走的,從裏到外,這從裏到外的各層做用域就組成了做用域鏈。
首先聲明一點,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
複製代碼
這個例子中有個三個嵌套的做用域,如圖:
A 爲全局做用域,有一個標識符:fn1
B 爲fn1所建立的做用域,有三個標識符:x、y、fn2
C爲fn2所建立的做用域,有一個標識符:z
做用域是由期代碼寫在哪裏決定的,而且是逐級包含的。
在此強調,詞法做用域就是做用域是由書寫代碼時函數聲明的位置來決定的。編譯階段就可以知道所有標識符在哪裏以及是如何聲明的,因此詞法做用域是靜態的做用域,也就是詞法做用域可以預測在執行代碼的過程當中如何查找標識符。
注1:eval()和with能夠經過其特殊性用來「欺騙」詞法做用域,不過正常狀況下都不建議使用,會產生性能問題。
注2:ES6中有了let、const就有了塊級做用域,後面會專門介紹。
能夠關注個人公衆號:icemanFE,接下來持續更新技術文章!