July 2, 2023

Viiisit [JavaScript] - Hoisting & Scope!

#javascript

繼上篇提到的宣告變數與常數後,透過上篇的總結表格,來聊聊變數提升以及作用域!

Hoisting

在說明 Block Scope (區塊作用域) & Function Scope (函式作用域) 之前,
先來提及 Hoisting (提升),了解 Hoisting (提升) 可以幫助我們更加了解 JavcaScript!

What is hoisting?

Hoisting (提升) 是 Javascript 在程式的編譯階段,
會先把宣告的變數和函式放在程式的頂端,等到實際執行時在賦予其值。

在 JavaScript,如果試圖對一個還沒宣告的變數取值,會發生:
Uncaught ReferenceError: i is not defined
這是因為沒有宣告,JavaScript 也找不到這個變數!

1
console.log(i); // Uncaught ReferenceError: i is not defined

但,詭異的事情發生了!
console.log(l); 之後才宣告一個變數,這會使得結果變為 undefined
這樣的效果就是 Hoisting ,因為發生在變數身上,因此也稱為 Variable Hoisting (變數提升)。

1
2
console.log(l); // undifined
var l = 2;

會造成以上的原因,要了解 JavaScript 在運作上到底是做了什麼事!☟

Creation Phase(創建期) & Execution Phase(執行期)

當 JavaScript 執行一段程式碼時,會創造 Execution Contexts

由於 JavaScript 屬於單執行緒 (Single Thread),意即「一次只能做一個任務」,
每個任務都有一個執行環境(execution context),JavaScript 引擎用 call stack 來追蹤它們。

在 JavaScript 中,有兩種 Execution Contexts (執行環境):
Global Excution Context 全域執行環境
執行任何程式之前,預設會建立的一個全域環境。
Function Excution Context 函式執行環境
每呼叫函式一次,就會建立一個新的函式執行環境,負責處理函式中的程式碼。

在運作上分兩個主階段,Creation Phase(創建期) 跟 Execution Phase(執行期)。
所有程式碼在 Creation Phase(創建期)跑完才會進行 Execution Phase(執行期)

  1. var 生命週期

    1
    2
    console.log(l); // undifined
    var l = 2;
    • Creation Phase: a. Declaration b. Initialization
      在 var 的生命週期裡,創建期會執行:a. 宣告 b. 初始化
      line 1: console.log(l); 在 Creation Phase(創建期)不會執行,
      code 會繼續往下到 line 2: var l = 2;
      此時,Declaration (宣告) 會完成並記錄 l 這個變數,
      同時,Initialization (初始化) 會賦予 l 這個變數 undefined。

    • Execution Phase: Assignment
      接下來進入 Execution Phase(執行期):賦值,
      line 1: console.log(l); 因為沒有 Assignment(賦值),
      因此,在 Initialization 被賦予的 undefined 就被 console.log 出來。
      code 繼續往下到 line 2: var l = 2;
      此時,才會完成 Assignment(賦值),而 l 的數值才改變為 2。

  2. let & const 生命週期

    1
    2
    console.log(l); // Uncaught ReferenceError: Cannot access 'l' before initialization
    let l = 2;
    1
    2
    console.log(l); // Uncaught ReferenceError: Cannot access 'l' before initialization
    const l = 2;
    • Creation Phase: a. Declaration
      在 let & const 的生命週期裡,創建期執行: a. 宣告
      line 1: console.log(l); 在 Creation Phase(創建期)不會執行,
      code 會繼續往下到 line 2: let l = 2; / const l = 2;
      此時,Declaration (宣告) 會完成並記錄 l 這個變數,
      注意!let & const 並沒有初始化的過程,所以 l 這個變數處於 not defined,無法被使用。

    • Execution Phase: Assignment
      接下來進入 Execution Phase(執行期):賦值,
      line 1: console.log(l); 因為沒有 Assignment(賦值),
      因此,在沒有初始化的狀態下 console.log,就會出現:
      Uncaught ReferenceError: Cannot access 'l' before initialization
      此時,因為 line 1 報錯,後面的 code 便無法進行。

      會造成這樣的原因,是因為 TDZ - Temporal Dead Zone!
      TDZ 可以防止使用一個尚未被賦值的變數;因此,在使用 let & const 要定義在執行之前!

      A variable declared with let, const, or class is said to be in a “temporal dead zone” (TDZ) from the start of the block until code execution reaches the line where the variable is declared and initialized. While inside the TDZ, the variable has not been initialized with a value, and any attempt to access it will result in a ReferenceError.
      Reference: MDN - Temporal dead zone (TDZ)


Scope 作用域

回歸到先前提及的 Block Scope (區塊作用域) & Function Scope (函式作用域)!

What is Scope?

在詳細介紹前,先來閱讀一下 MDN 的定義說明:

The scope is the current context of execution in which values and expressions are “visible” or can be referenced. If a variable or expression is not in the current scope, it will not be available for use. Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.
Reference: MDN - Scope

總括來說,作用域就是有效範圍,以下歸納兩個重點:


JavaScript 作用域有哪些?

JavaScript 有三種類型的作用域:

JavaScript 作用域在引入 ES6 之後,因為 let 與 const 的關係而有了 Block Scope (區塊作用域)。


Global Scope 全域作用域

全域作用域是 JavaScript 作用域的最外層(window 物件就是在這範圍裡),
在全域作用域宣告的變數或函式都可以在其他作用域取得,如下:

1
2
3
4
5
const number = 2;  // 全域變數
function getNumber() => {
console.log(number); // 2
}
console.log(number); // 2

要注意的是!
如果宣告很多變數、函式卻沒使用,他們就全部都在全域作用域中,會造成全域污染,容易引起命名衝突。

Function Scope 函式作用域

當建立一個函式,就會生成函式作用域;
不論是 const、let 或 var,當他們在函式中宣告時,皆屬於函式作用域。
在函式作用域裡的變數無法在函式外面取得,且該變數只存在於函式裡,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// const
function getNumber() {
const number = 2;
console.log(number); // 2
}
getNumber();
console.log(number); // Uncaught ReferenceError: number is not defined

// let
function getNumber() {
let number = 3;
console.log(number); // 3
}
getNumber();
console.log(number); // Uncaught ReferenceError: number is not defined

// var
function getNumber() {
var number = 4;
console.log(number); // 4
}
getNumber();
console.log(number); // Uncaught ReferenceError: number is not defined

Block Scope 區塊作用域

ES6 引入了 const, let 兩種宣告方式,而產生了區塊作用域。

區塊作用域的範圍只存在於 { }
在區塊作用域裡的變數無法在區塊外面取得,且該變數只存在於{ },如下:

1
2
3
4
5
6
7
function getNumber() {
if (true) {
const number = 2;
}
console.log(number); // Uncaught ReferenceError: number is not defined
}
getNumber();

const, let 作用於 Block Scope (區塊做用域); var 作用於 Function Scope (函式作用域)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// const
function getNumber() {
if (true) {
const number = 2;
}
console.log(number); // Uncaught ReferenceError: number is not defined
}
getNumber();

// let
function getNumber() {
if (true) {
let number = 3;
}
console.log(number); // Uncaught ReferenceError: number is not defined
}
getNumber();

// var 不會被 {} 限制
function getNumber() {
if (true) {
var number = 4;
}
console.log(number); // 4
}
getNumber();

Brief Summary:


參考資料:
JS 宣告變數, var 與 let / const 差異
MDN
ㄚ建的技能樹