深刻理解JavaScript做用域

在上一篇文章 深刻理解JavaScript 執行上下文 中提到 只有理解了執行上下文,才能更好地理解 JavaScript 語言自己,好比變量提高,做用域,閉包等,本篇文章就來講一下 JavaScript 的做用域。javascript

這篇文章稱爲筆記更爲合適一些,內容來源於 《你不知道的JavaScript(上卷)》第一部分 做用域和閉包。講的很不錯,很是值得一看。html

什麼是做用域

做用域是根據名稱查找變量的一套規則java

理解做用域

先來理解一些基礎概念:git

  • 引擎:從頭至尾負責整個JavaScript程序的編譯及執行過程。
  • 編譯器:負責語法分析和代碼生成。這部分也能夠看 JavaScript代碼是如何被執行的
  • 做用域:負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限。

接下來來看看下面代碼的執行過程:github

var a = 2;
  1. 碰見 var a,編譯器 會問 做用域 變量a是否存在於同一個做用域集合中。若是存在,編譯器會忽略聲明,繼續編譯;不然,會要求做用域在當前做用域集合中聲明一個新的變量,並命名爲 a
  2. 接下來 編譯器 會爲 引擎 生成運行時所需的代碼,用來處理 a = 2 這個賦值操做。引擎運行時會先問做用域,當前做用域集中是否存在變量a。若是是,引擎就會使用該變量;若是不存在,引擎會繼續查找該變量
  3. 若是 引擎 找到了a 變量,就會將 2 賦值給它,不然引擎就拋出一個錯誤。

總結:變量的賦值操做會執行兩個動做,首先編譯器會在當前做用域中聲明一個變量,而後在運行時引擎就會會做用域中查找該變量,若是可以找到就對它賦值。segmentfault

編譯器在編譯過程的第二步中生成了代碼,引擎執行它時,會經過查找變量 a來判斷它是否已聲明過。查找的過程當中由做用域進行協助,可是引擎執行怎麼樣的查找,會影響最終的查找結果。性能優化

在咱們的例子中,引擎會爲變量 a 進行 LHS 查詢,另一個查找的類型叫作 RHS。 」L「 和 "R" 分別表明一個賦值操做左側和右側。當變量出如今賦值操做的左側時進行 LHS 查詢,出如今右側時進行 RHS 查詢。閉包

LHS:試圖找到變量的容器自己,從而能夠對其賦值;RHS: 就是簡單地查找某個變量的值。
console.log(a);

對 a 的引用是一個 RHS 引用,由於這裏 a 並無賦予任務值,相應地須要查找並取得 a 的值,這樣才能將值傳遞給 console.log(...)函數

a = 2;

這裏對 a 的引用是 LHS 引用,由於實際上咱們並不關心當前的值是什麼,只是想要爲 = 2這個賦值操做找到目標。post

funciton foo(a) {
    console.log(a)
}

foo(2);
  1. 最後一行 foo 函數的調用須要對 foo 進行 RHS 引用,去找 foo的值,並把它給我
  2. 代碼中隱式的 a = 2 操做可能很容易被你忽略掉,這操做發生在 2 被當作參數傳遞給 foo 函數時,2 會被分配給參數 a,爲了給參數 a (隱式地) 分配值,須要進行一次 LHS 查詢。
  3. 這裏還有對 a 進行的 RHS 引用,而且將獲得的值傳給了 console.log(...)console.log(...) 自己也須要一個引用才能執行,所以會對 console對象進行 RHS 查詢,而且檢查獲得的值中是否有一個叫作 log的方法。

RHS查詢在全部嵌套的做用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError 異常。進行RHS查詢找到了一個變量,可是你嘗試對這個變量的值進行不合理的操做,好比試圖對一個非函數類型的值進行調用,後者引用null或 undefined 類型的值中的屬性,那麼引擎會拋出一個另一種類型的異常 TypeError。
引擎執行 LHS 查詢時若是找不到該變量,則會在全局做用域中建立一個。可是在嚴格模式下,並非自動建立一個全局變量,而是會拋出 ReferenceError 異常

補充 JS幾種常見的錯誤類型

簡單總結以下:

做用域是一套規則,用於肯定在哪裏找,怎麼找到某個變量。若是查找的目的是對變量進行賦值,那麼就會使用 LHS查詢; 若是目的是獲取變量的值,就會使用 RHS 查詢;
JavaScript 引擎執行代碼前會對其進行編譯,這個過程當中,像 var a = 2 這樣的聲明會被分解成兩個獨立的步驟

  1. var a 在其做用域中聲明變量,這會在最開始的階段,也就是代碼執行前進行
  2. 接下來,a = 2 會查詢 (LHS查詢)變量 a 並對其進行賦值。

詞法做用域

詞法做用域是你在寫代碼時將變量寫在哪裏來決定的。編譯的詞法分析階段基本可以知道全局標識符在哪裏以及是如何聲明的,從而可以預測在執行過程當中若是對他們查找。

有一些方法能夠欺騙詞法做用域,好比 eval, with, 這兩種如今被禁止使用,1是嚴格模式和非嚴格模式下表現不一樣 2是有性能問題, JavaScript引擎在編譯階段會作不少性能優化,而其中不少優化手段都依賴於可以根據代碼的詞法進行靜態分析,並預先肯定全部變量和函數的定義位置,才能在執行過程當中快速找到識別符,eval, with會改變做用域,因此碰到它們,引擎將沒法作優化處理。

全局做用域和函數做用域

全局做用域

  • 在最外層函數和最外層函數外面定義的變量擁有全局做用域
var a = 1;
function foo() {

}

變量a 和函數聲明 foo 都是在全局做用域中的。

  • 全部未定義直接賦值的變量自動聲明爲擁有全局做用域
var a = 1;
function foo() {
    b = 2;
}
foo();
console.log(b); // 2
  • 全部 window 對象的屬性擁有全局做用域

函數做用域

函數做用域是指在函數內聲明的全部變量在函數體內始終是可見的。外部做用域沒法訪問函數內部的任何內容。

function foo() {
    var a = 1;
    console.log(a); // 1
}
foo();
console.log(a); // ReferenceError: a is not defined
只有函數的 {}構成做用域,對象的 {}以及 if(){}都不構成做用域;

變量提高

提高是指聲明會被視爲存在與其所出現的做用域的整個範圍內。

JavaScript編譯階段是找到找到全部聲明,並用合適的做用域將他們關聯起來(詞法做用域核心內容),因此就是包含變量和函數在內的全部聲明都會在任何代碼被執行前首先被處理。

每一個做用域都會進行提高操做。

function foo() {
    var a;
    console.log(a); // undefined
    a = 2;
}
foo();
注意,函數聲明會被提高,可是函數表達式不會被提高。

關於 塊級做用域和變量提高的內容以前在 從JS底層理解var、let、const這邊文章中詳細介紹過,這裏再也不贅述。

塊級做用域

咱們來看下面這段代碼

for(var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    })
}
console.log(`當前的i爲${i}`); // 當前的i爲5

上面這段代碼咱們但願是輸出 0,1, 2, 3, 4 ,可是實際上輸出的是 5,5, 5, 5, 5。咱們在 for 循環的頭部直接定義了變量 i,一般是由於只想在 for 循環內部的上下文中使用 i,可是實際上 此時的 i 被綁定在外部做用域(函數或全局)中。

,塊級做用域是指在指定的塊級做用域外沒法訪問。在ES6以前是沒有塊級做用域的概念的,ES6引入了 let 和 const。咱們能夠改寫上面的代碼,使它按照咱們想要的方式運行。

for(let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    })
}
// 0 1 2 3 4
console.log(`當前的i爲${i}`); // ReferenceError: i is not defined

此時 for 循環頭部的 let 不只將 i 綁定到了 for 循環的迭代中,事實上將它從新綁定到了循環的每個迭代中,確保使用上一次循環迭代結束的值從新進行賦值。

let聲明附屬於一個新的做用域而不是當前的函數做用域(也不屬於全局做用域)。可是其行爲是同樣的,能夠總結爲:任何聲明在某個做用域內的變量,都將附屬於這個做用域。
const也是能夠用來建立塊級做用域變量,可是建立的是固定值。

做用域鏈

JavaScript是基於詞法做用域的語言,經過變量定義的位置就能知道變量的做用域。全局變量在程序中始終都有都定義的。局部變量在聲明它的函數體內以及其所嵌套的函數內始終是有定義的。

每一段 JavaScript 代碼都有一個與之關聯的做用域鏈(scope chain)。這個做用域鏈是一個對象列表或者鏈表。當 JavaScript 須要查找變量 x 的時候(這個過程稱爲變量解析),它會從鏈中的第一個變量開始查找,若是這個對象上依然沒有一個名爲 x 的屬性,則會繼續查找鏈上的下一個對象,若是第二個對象依然沒有名爲 x 的屬性,javaScript會繼續查找下一個對象,以此類推。若是做用域鏈上沒有任何一個對象包含屬性 x, 那麼就認爲這段代碼的做用域鏈上不存在 x, 並最終拋出一個引用錯誤 (Reference Error) 異常。

下面做用域中有三個嵌套的做用域。

function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log(a, b, c)
    }
    bar( b * 3);
}
foo(2);

氣泡1包含着整個全局做用域,其中只有一個標識符:foo;
氣泡2包含着foo所建立的做用域,其中有三個標識符:a、bar 和 b;
氣泡3包含着 bar所建立的做用域,其中只有一個標識符:c

執行 console.log(...),並查找 a,b,c三個變量的引用。下面咱們來看看查找這幾個變量的過程.
它首先從最內部的做用域,也就是 bar(..) 函數的做用域氣泡開始找,引擎在這裏沒法找到 a,所以就會去上一級到所嵌套的 foo(...)的做用域中繼續查找。在這裏找到了a,所以就使用了這個引用。對b來講也同樣,而對 c 來講,引擎在 bar(..) 中就找到了它。

若是 a,c都存在於 bar(...) 內部,console.log(...)就能夠直接使用 bar(...) 中的變量,而無需到外面的 foo(..)中查找。做用域會在查找都第一個匹配的標識符時就中止。

在多層的嵌套做用域中能夠定義同名的標識符,這叫」遮蔽效應「。

var a = '外部的a';
function foo() {
    var a = 'foo內部的a';
    console.log(a); // foo內部的a
}
foo();

做用域與執行上下文

JavaScript的執行分爲:解釋和執行兩個階段

解釋階段

  • 詞法分析
  • 語法分析
  • 做用域規則肯定

執行階段

  • 建立執行上下文
  • 執行函數代碼
  • 垃圾回收

做用域在函數定義時就已經肯定了,而不是在函數調用時肯定,但執行上下文是函數執行以前建立的。

總結

  1. 做用域就是一套規則,用於肯定在哪裏找以及怎麼找到某個變量。
  2. 詞法做用域在你寫代碼的時候就肯定了。JavaScript是基於詞法做用域的語言,經過變量定義的位置就能知道變量的做用域。ES6引入的let和const聲明的變量在塊級做用域中。
  3. 聲明提高是指聲明會被視爲存在與其所出現的做用域的整個範圍內。
  4. 查找變量的時候會先從內部的做用域開始查找,若是沒找到,就往上一級進行查找,依次類推。
  5. 做用域在函數定義時就已經肯定了,執行上下文是函數執行以前建立的。

參考

相關文章
相關標籤/搜索