iT邦幫忙

2023 iThome 鐵人賽

DAY 4
0

繼上篇提到的提升與作用域後,今天來點 Scope Chain (作用域鏈) 與 Closure (閉包)!

Scope Chain(作用域鏈)是什麼?

當 JavaScript 使用每一個變數時,會先嘗試在目前的作用域中尋找該變數;
若找不到該變數,便會一直往外層作用域尋找,直到全域作用域還是沒找到的話就會直接報錯。
這一層一層的關係,就是作用域鏈。
以下是一段簡單的範例:

let a = 100;
function getNumber() {
  // 在 getNumber 函式作用域中沒有變數 a,於是透過作用域鏈往外層尋找,
  // 在這邊的外層是全域,也就找到了 a 變數
  console.log(a); // 100
}
getNumber();

Closure(閉包)是什麼?

閉包(Closure)是 JavaScript 中的一個重要概念,每當創建一個函式時,都會同時創建一個閉包。
閉包(Closure)是一個函式和此函式被宣告時所在的詞法環境組合而成的。

詞法環境(Lexical Environment)是 JavaScript 用於管理變數、函數和作用域。當函數被創建時,同時也會創建一個詞法環境,這個環境記錄了函數內部的變數、函數聲明以及對外部作用域的引用。

來看一個簡單的例子:

function say(greeting) {

  return function(name) {
    console.log(greeting + ' ' + name);
  }

}

var host = say('Welcome');
host('Viii'); // Welcome Viii

大家是否還記得上篇提及的 Execution Context,當 JavaScript 執行以上程式碼時:

  1. 開始執行 Global Execution Context,因為 JavaScript 中的 hoisting(提升),function say 已經被建立且儲存在記憶體中,並且可以在作用域中使用。

  2. Global Execution Context 繼續執行 var host = say('Welcome');。在執行 say('Welcome') 時,一個新的 Execution Context 被建立,greeting 也被儲存在 function say 的 Execution Context 中。 透過 say 函式,內部建立一個匿名函式。此時,function say 也就執行完了。雖然 Execution Context 已經不在了,但其中的變數還是儲存在那個記憶體位置!

  3. 結束前一個任務後,繼續回到 Global Execution Context,接著,碰到了 host('Viii') 於是我們建立了一個給匿名函式的 Execution Context,同時裡面帶有參數 name,因為此時在自己的 function 裡面找不到 greeting 這個變數,所以會開始透過 Scope Chain 尋找。因為前面有提及 function say 在記憶體位置仍留有參照,所以在 function say 裡面所建立的函式仍然可以找得到 greeting 這個變數。最後就印出結果 Welcome Viii

Brief Summary

可以在第三點看出,匿名函示式的 Execution Context 與外面的變數 greeting close 在一起了,這就是閉包 (Closure)。透過這樣的特性,可以確保當在執行 function 的時候,JavaScript 能夠找到其相對應的變數。


透過閉包讓 function 擁有 private 變數

理解閉包的概念後,可以透過閉包讓 function 擁有 private 變數,
在這個例子中,chocolateCount 變數被保護在 chocolateBox 函式的作用域內,
只能通過 eatChocolate 函式來訪問和修改。
外部的程式碼無法直接訪問或修改 chocolateCount 變數。

function chocolateBox() {
  let chocolateCount = 10; // 總共有10顆巧克力,這是函式內要調用的變數

  function eatChocolate() {
    if (chocolateCount > 0) {
      chocolateCount--;
      console.log(`吃了一顆巧克力,還剩下 ${chocolateCount} 顆。`);
    } else {
      console.log("巧克力已經吃完了!");
    }
  }

  return eatChocolate;
}

const eatChocolate = chocolateBox();

eatChocolate(); // 吃了一顆巧克力,還剩下 9 顆。
eatChocolate(); // 吃了一顆巧克力,還剩下 8 顆。
eatChocolate(); // 吃了一顆巧克力,還剩下 7 顆。
// 依此類推...
eatChocolate(); // 吃了一顆巧克力,還剩下 1 顆。
eatChocolate(); // 吃了一顆巧克力,還剩下 0 顆。
eatChocolate(); // 巧克力已經吃完了!

確保了私有變數在不同的執行環境中是獨立的

閉包的一個重要特性,確保私有變數在不同的執行環境中是獨立的。
創建出兩個不同的 box1 和 box2,並且都是使用 chocolateBox 函式創建。
即使使用了相同的函式,但每個 box 都有自己獨立的 chocolateCount 變數,
因此彼此之間不會互相影響,每個盒子都可以獨立地操作巧克力數量。

function chocolateBox() {
  let chocolateCount = 10;

  function eatChocolate() {
    if (chocolateCount > 0) {
      chocolateCount--;
      console.log(`吃了一顆巧克力,還剩下 ${chocolateCount} 顆。`);
    } else {
      console.log("巧克力已經吃完了!");
    }
  }

  return eatChocolate;
}

const box1 = chocolateBox();
const box2 = chocolateBox();

box1(); // 吃了一顆巧克力,還剩下 9 顆。
box2(); // 吃了一顆巧克力,還剩下 9 顆。

box1(); // 吃了一顆巧克力,還剩下 8 顆。
box2(); // 吃了一顆巧克力,還剩下 8 顆。

// 每個盒子都有獨立的 chocolateCount 變數,不會互相影響

今天簡單地講述作用域鏈與閉包,下篇會繼續補充閉包的應用以及相關重要小知識!下篇待續!

(持續理解所有的為什麼! go! go! go!)ヽ(●>∀<●)ノ


參考資料:

文章同步於個人部落格:Viiisit!(歡迎參觀 ୧ʕ•̀ᴥ•́ʔ୨)


上一篇
Day 03 - 理解 JavaScript ,為什麼要知道變數提升與作用域?
下一篇
Day 05 - 理解 JavaScript ,為什麼要知道閉包(下)?
系列文
從零開始,在 coding 路上的 30 個為什麼?不對!是無數個為什麼!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言