講解 JavaScript 做用域的文章也有不少了,這裏我想聊聊一些不同的東西。會的讀者能夠複習,不會的同窗能夠了解。javascript
在 JavaScript 中,咱們能夠經過
var
、let
來聲明變量,也能夠經過const
來定義常量。html可是在 JavaScript 中,變量的做用域一直都很複雜。java
在 JavaScript ES3 中,咱們只能經過 var
來聲明變量,變量在聲明時會有變量提高(hoisting),即在後面聲明的變量能夠被提早訪問,而值默認爲 undefined
。瀏覽器
在 ES3 中,最外層的做用域稱爲全局做用域。若是你在全局做用域下聲明變量,這些變量都會被添加到一個全局對象 globalThis
上,成爲它的一個屬性。函數
這個 globalThis
在不一樣環境下指代不一樣的目標,好比在 Node.JS 中,globalThis
就是 global
;在瀏覽器下,globalThis
就是 window
,而且也能夠經過 self
來訪問。ui
除了全局做用域之外,ES3 還有三種局部做用域:spa
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
複製代碼
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
}
複製代碼
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 中引入了兩個新的變量/常量定義方法:let
和 const
。由 let
和 const
聲明/定義的變量/常量沒有提高,而且僅在當前塊中有效,也就是說,他們是塊級做用域。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
複製代碼
這裏 const
和 let
聲明/定義的變量/常量僅在 if
塊中能夠訪問。對象
甚至,你能夠直接寫一個塊:
{
const a = '🍐';
let b = '🍐';
}
複製代碼
那麼,若是混合 var
和 const
,會發生什麼?
(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 來講的。
首先,a
、b
、c
都在全局做用域下,第二個 <script>
也是在全局做用域下的,因此是能夠直接訪問三個變量/常量的。
可是在 ES6 中,let
和 const
即使是在全局做用域下聲明/定義,也不會將其添加到全局對象上去,因此若是在第二個標籤中去經過 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"
屬性,裏面嘗試訪問並打印 a
、b
、c
、d
、e
、f
六個變量/常量;而後又是一個 <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>
標籤,聲明定義了 d
、e
、f
三個變量/常量。
再以後,被延遲的代碼塊開始依次執行,先是第一個代碼塊,聲明定義了 a
、b
、c
三個變量/常量,可是因爲它是一個 module,所以全部變量/常量僅對自身 module 可見,對外部均不可訪問。
最後,是被延遲的第二個代碼塊,執行這個代碼塊會先建立一個 console group,輸出一個 A
,而後開始依次訪問全部變量/常量。其中 a
、b
、c
處於其餘 module 中,所以沒法訪問,而 d
、e
、f
均已聲明/定義,所以能夠正常訪問。
注意,這裏不是說 let
和 const
發生了提高,而僅僅是受到 defer
效果而使得執行順序發生了改變。