窺探 JavaScript 中的變量做用域

講解 JavaScript 做用域的文章也有不少了,這裏我想聊聊一些不同的東西。會的讀者能夠複習,不會的同窗能夠了解。javascript

在 JavaScript 中,咱們能夠經過 varlet 來聲明變量,也能夠經過 const 來定義常量。html

可是在 JavaScript 中,變量的做用域一直都很複雜。java

目錄

  • 一、熱身:ES3 的做用域
  • 二、ES6 的做用域
  • 三、瀏覽器的全局做用域
  • 四、模塊做用域

一、熱身:ES3 的做用域

在 JavaScript ES3 中,咱們只能經過 var 來聲明變量,變量在聲明時會有變量提高(hoisting),即在後面聲明的變量能夠被提早訪問,而值默認爲 undefined瀏覽器

在 ES3 中,最外層的做用域稱爲全局做用域。若是你在全局做用域下聲明變量,這些變量都會被添加到一個全局對象 globalThis 上,成爲它的一個屬性。函數

這個 globalThis 在不一樣環境下指代不一樣的目標,好比在 Node.JS 中,globalThis 就是 global;在瀏覽器下,globalThis 就是 window,而且也能夠經過 self 來訪問。ui

除了全局做用域之外,ES3 還有三種局部做用域:spa

  1. 函數做用域 在 ES3 中的函數中,使用 var 來聲明變量,全部變量都會提高至函數開頭,而且只能在當前函數塊內部訪問。
    var a = '🍐';
    (function() {
      console.log(a, b); // 🍐, undefined
      var b = '🍐';
      console.log(b); // 🍐
    })();
    console.log(a); // 🍐
    try { console.log(b) } catch(e) { console.error(e.message) } // b is not defined
    複製代碼
  2. catch 做用域 在 try { ... } catch(e) { ... } finally { ... } 語句中,變量 e 僅在 catch 塊中能夠訪問。
    try {
      try { console.log(err) } catch(e) { console.error(e.message) } // err is not defined
      throw Error('🍐');
    } catch(err) {
      console.log(err); // Error: 🍐
    } finally {
      try { console.log(err) } catch(e) { console.error(e.message) } // err is not defined
    }
    複製代碼
  3. with 做用域 雖然 with 已經不推薦使用了,而且在嚴格模式(use strict)中已經不可使用了,可是 with 確實會創造一個局部做用域環境。在 with (obj) {} 語句中,JavaScript 會爲 obj 上全部的屬性都建立一個局部變量,全部這些變量都只能夠在 with 塊中訪問。
    try { console.log(sin) } catch(e) { console.error(e.message) } // sin is not defined
    with(Math) {
      console.log(sin); // function sin() { [native code] }
    }
    try { console.log(sin) } catch(e) { console.error(e.message) } // sin is not defined
    複製代碼

二、ES6 的做用域

在 ES6 中引入了兩個新的變量/常量定義方法:letconst。由 letconst 聲明/定義的變量/常量沒有提高,而且僅在當前塊中有效,也就是說,他們是塊級做用域。code

當你嘗試在聲明變量/常量以前訪問它時,它會提示你「不能在初始化以前訪問」,而不是「變量未定義」。這種現象被叫作「臨時死區」,而不是「變量提高」。htm

if (true) {
  try { console.log(a); } catch(e) { console.error(e.message) } // Cannot access 'a' before initialization
  try { console.log(b); } catch(e) { console.error(e.message) } // Cannot access 'b' before initialization
  const a = '🍐';
  let b = '🍐';
  console.log(a, b); // 🍐 🍐
}
try { console.log(a); } catch(e) { console.error(e.message) } // a is not defined
try { console.log(b); } catch(e) { console.error(e.message) } // b is not defined
複製代碼

這裏 constlet 聲明/定義的變量/常量僅在 if 塊中能夠訪問。對象

甚至,你能夠直接寫一個塊:

{
  const a = '🍐';
  let b = '🍐';
}
複製代碼

那麼,若是混合 varconst,會發生什麼?

(function() {
  var a = '🍐';
  {
    var b = '🍐';
    const a = '🍐🍐';
    const b = '🍐'; // Uncaught SyntaxError: Identifier 'b' has already been declared
  }
})();
複製代碼

咱們能夠看到,即使 a 已經聲明/定義,在獨立的塊中也可使用 const 來覆蓋,從新定義。在塊中 a 的值是 🍐🍐,可是離開塊後,a 的值仍是 🍐

可是,若是在這個獨立的塊中,使用 var 聲明瞭一個變量 b,雖然說 b 會提高至 function 層,可是,在語法解釋階段 const b 就會失敗,由於在同一個塊中已經聲明瞭 b

三、瀏覽器的全局做用域

在瀏覽器中,HTML 容許咱們使用 <script> 包裹 JavaScript 代碼,而且在同一個 HTML 文檔中能夠放置多個 <script> 標籤。

考慮這段代碼:

<script> var a = '🍐'; let b = '🍐'; const c = '🍐'; </script>
<script> console.log(a); console.log(b); console.log(c); console.log(self.a); console.log(self.b); console.log(self.c); </script>
複製代碼

有兩個 <script> 標籤,第一個裏面聲明/定義了三個變量/常量,第二個裏面花式訪問這些變量/常量,會發生什麼?答案是:

🍐
🍐
🍐
🍐
undefined
undefined
複製代碼

結果前 4 個輸出了🍐,然後兩個輸出了 undefined

在前面說過,若是你在全局做用域聲明瞭變量,它會被自動添加到全局對象上去。

可是這僅僅是針對 ES3 來講的。

首先,abc 都在全局做用域下,第二個 <script> 也是在全局做用域下的,因此是能夠直接訪問三個變量/常量的。

可是在 ES6 中,letconst 即使是在全局做用域下聲明/定義,也不會將其添加到全局對象上去,因此若是在第二個標籤中去經過 self 訪問是不存在的。

若是訪問不存在的變量,會拋出異常;可是僅僅是訪問不存在的屬性就不要緊,所以後兩個返回 undefined

四、模塊做用域

考慮這段代碼:

<script type="module"> var a = '🍐'; let b = '🍐'; const c = '🍐'; </script>
<script type="module"> console.group('A'); try { console.log(a) } catch(e) { console.error(e.message) } try { console.log(b) } catch(e) { console.error(e.message) } try { console.log(c) } catch(e) { console.error(e.message) } try { console.log(d) } catch(e) { console.error(e.message) } try { console.log(e) } catch(e) { console.error(e.message) } try { console.log(f) } catch(e) { console.error(e.message) } console.groupEnd(); </script>
<script defer> console.group('B'); try { console.log(a) } catch(e) { console.error(e.message) } try { console.log(b) } catch(e) { console.error(e.message) } try { console.log(c) } catch(e) { console.error(e.message) } try { console.log(d) } catch(e) { console.error(e.message) } try { console.log(e) } catch(e) { console.error(e.message) } try { console.log(f) } catch(e) { console.error(e.message) } console.groupEnd(); </script>
<script> var d = '🍐'; let e = '🍐'; const f = '🍐'; </script>
複製代碼

首先,一個 <script> 標籤,擁有 type="module" 屬性,裏面聲明定義了幾個變量/常量;而後,跟着一個 <script> 標籤,一樣擁有 type="module" 屬性,裏面嘗試訪問並打印 abcdef 六個變量/常量;而後又是一個 <script> 標籤,內容與第二個幾乎同樣,除了 group 的內容,沒有 type="module" 屬性,卻多了 defer 屬性;最後仍是一個 <script> 標籤,除了沒有 type="module" 屬性外,內容與第一個徹底同樣。

運行結果會怎樣?答案是:

┏ B
┣ a is not defined
┣ b is not defined
┣ c is not defined
┣ d is not defined
┣ e is not defined
┗ f is not defined
┏ A
┣ a is not defined
┣ b is not defined
┣ c is not defined
┣ 🍐
┣ 🍐
┗ 🍐
複製代碼

猜對了嗎?

這裏涉及到四個知識點:受到 defer 做用的代碼塊會被延遲到最後執行;type="module"<script> 默認包含 defer 行爲,而且裏面定義的變量/常量做用域都是僅影響局部的;對於 inline 內聯的 <script> 而言,defer 屬性會被忽略。

先看第一個代碼塊,type="module",所以代碼被延遲執行。

而後是第二個代碼塊,一樣是 type="module",代碼被延遲執行。

再看第三個代碼塊,因爲這是個內聯的腳本(內容直接在標籤內給出而不是經過 src 屬性指定),所以 defer 屬性被忽略,這個腳本仍是以正常順序執行。執行這個代碼塊會先建立一個 console group,輸出一個 B,而後開始依次訪問全部變量/常量。可是,看看上面兩個代碼塊,都被延遲執行了,所以此時全部變量都未定義。

而後是第四個代碼塊,一個普通的 <script> 標籤,聲明定義了 def 三個變量/常量。

再以後,被延遲的代碼塊開始依次執行,先是第一個代碼塊,聲明定義了 abc 三個變量/常量,可是因爲它是一個 module,所以全部變量/常量僅對自身 module 可見,對外部均不可訪問。

最後,是被延遲的第二個代碼塊,執行這個代碼塊會先建立一個 console group,輸出一個 A,而後開始依次訪問全部變量/常量。其中 abc 處於其餘 module 中,所以沒法訪問,而 def 均已聲明/定義,所以能夠正常訪問。

注意,這裏不是說 letconst 發生了提高,而僅僅是受到 defer 效果而使得執行順序發生了改變。

記得要點贊、分享、評論三連,更多精彩內容請關注ihap 技術黑洞!
相關文章
相關標籤/搜索