JavaScript 入門必學觀念:深入理解變數的作用範圍 (Scope)

Published July 11, 2025 by 徐培鈞
JavaScript

在學習 JavaScript 的過程中,變數(Variable) 是你最常使用也最基礎的概念之一。

每當你要儲存資料、處理邏輯、或撰寫函式時,變數就會登場。

而在使用變數時,有一個非常重要但常被忽略的核心概念,那就是:變數的作用範圍(Scope)

簡單來說,作用範圍就是「某個變數在程式的哪些地方能夠被讀取或使用」

你可以把作用範圍想像成通行證制度:變數就像一個地區限定的票證或門票,只能在特定區域內使用。

例如,一張捷運票只能在進站後的車站區域內有效,離開閘門後就不能再用了。

同樣地,在程式中,變數也只能在它被「發行」的範圍內被讀取或操作,離開那個範圍,它就不再有效了。

什麼是作用範圍(Scope)?

在 JavaScript 中,當你宣告一個變數時,這個變數不會對整個程式「公開」

它只能在某些地方被「看到」或「使用」,這個可見的範圍就叫做「作用範圍(scope)」。

你可以把它想像成變數有一張通行證,只能在特定的區域內通行。

離開那個範圍,這張票就無效了。這樣的設計可以讓程式更有結構,變數之間不容易「打架」,也能避免無意間改錯值。

為什麼要有作用範圍呢?

答案其實很簡單 —— 如果所有變數都能到處用,程式就會變得非常難懂,而且很容易出錯。

來看看幾個例子:

✅ 1. 避免同名變數打架(也就是變數互相干擾)

想像你在寫一個購物網站的程式,某段程式碼負責計算購物車的總金額,你用了變數 total 來表示這個數字。

另一段程式則負責統計整個網站的營收報表,你也用了同樣的變數名稱 total,來記錄整個月的總營收。

如果這兩個 total 沒有各自的範圍限制,而是放在同一個共用的範圍中,那麼它們就會互相影響。

可能你在處理購物車時改了 total,結果無意間也把報表的數值改掉,讓資料顯示錯誤,甚至導致 bug 難以追查。

有了作用範圍,這兩個 total 就可以各自存在於不同的區塊中,互不干擾。你可以自由地使用相同的變數名稱,也不用擔心一邊的邏輯會破壞到另一邊的資料。

✅ 2. 幫助你看懂變數從哪裡來、降低找錯的難度

想像你正在讀一段程式碼,看到裡面使用了一個變數叫做 userName,但你不確定它是什麼,也不知道它在哪裡被設定或修改過。

你可能會問:「這個變數到底是從哪裡來的?什麼時候被改過?是我改的,還是別人改的?」

如果 JavaScript 沒有作用範圍,變數可以在整份程式的任何地方被宣告、改值或覆蓋,這樣你就必須把整份檔案都翻過一遍,才有可能查出問題。

這在程式很短的時候還能接受,但如果你的程式碼有幾百行甚至幾千行,找一個變數的來源就像在大海撈針一樣痛苦,也非常容易出錯。

有了作用範圍後,變數的活動空間就被限制住了。

例如 userName 是在某個函式裡宣告的,那你就可以放心地知道:這個變數只存在這個函式裡,外面不會動到它。

這樣不但能更快看懂變數的用途,也能大幅減少找錯與除錯的時間。

✅ 3. 限制變數的使用範圍,避免資料外洩或意外修改

有時候,你會在某個函式中處理比較敏感的資料,例如使用者的密碼。

你可能會這樣寫:

function login(password) {
  // 驗證邏輯
}

這裡的 password 是一個你只想在登入流程中使用的變數,但如果 JavaScript 沒有作用範圍的限制,這個變數就有可能「洩漏」到其他地方被看見或誤用。

舉例來說,其他程式區塊不小心也用了同樣名稱的 password,結果不但改壞了資料,還可能把原本的密碼印出來或傳錯地方,造成安全漏洞。

有了作用範圍後,這個 password 只會存在於 login() 函式內部。

外部完全看不到它,也不能使用它。當函式執行完畢後,這個變數就會自動消失。

這不只能保護敏感資料,也讓你更放心地在不同地方使用同樣的變數名稱,而不怕彼此衝突或被誤改。

在 JavaScript 中,主要有兩種常見的作用範圍:

全域變數(Global Variables):整份程式都能使用的變數

在 JavaScript 中,當你在最外層(也就是所有函式或區塊之外)宣告一個變數,它就會成為全域變數(global variable)

這種變數會「掛」在整個程式的最外層,所以在任何地方幾乎都能使用它,無論你是在上面、下面、函式裡還是邏輯區塊中,都可以存取。

來看一個例子:

let greeting = "Hello!";

function sayHi() {
  console.log(greeting); // → Hello!
}

sayHi();
console.log(greeting); // → Hello!

這裡的 greeting 是在函式外部宣告的變數,因此它是全域變數。你可以在函式內部讀取它,也可以在程式的其他地方使用它,這就是「全域」的意思。

使用全域變數的優點與風險

因為全域變數「到哪裡都能用」,所以在寫程式初期時會覺得它非常方便。

不管你在哪裡需要某個資料,只要在最外層宣告一次,全程都能直接使用,省下不少傳遞的麻煩。

但是,這種「到處都能用」的特性也可能造成問題,特別是當你的程式變得越來越大時:

🔸 不小心被其他地方改值

全域變數的最大風險就是,它可以被整個程式的任何地方修改。

這代表你在一個地方設定好的變數,可能會被另一個看似無關的程式片段改掉,造成錯誤卻難以發現。

🔸 隨著程式變大,變數變難管理

如果你把太多變數都設成全域的,久而久之你會搞不清楚誰用了什麼名稱,誰又不小心改了誰的值。

這會讓除錯變得非常困難。

🔸 程式容易互相干擾,尤其在多人協作時

你可能定義了一個全域變數叫 data,同事也剛好定義了一個一樣的 data,結果程式一跑,誰都搞不清楚現在的 data 是哪一個,產生難以預測的錯誤。

建議:盡量不要濫用全域變數

雖然全域變數在某些情況下很實用,例如設定常數(const APP_NAME = "MyApp")或需要整個程式共享的資料。

但在大多數情況下,更推薦使用在特定範圍內才會存在的變數,這樣可以讓你的程式更有組織,也更安全、更好除錯。

區域變數(Local Variables):只在特定範圍內有效的變數

和全域變數相反,區域變數是只在特定範圍內有效的變數。

這個範圍可以是兩種常見情況之一:

  1. 一個「函式」的範圍(function scope)
  2. 一對大括號 {} 組成的「區塊」範圍(block scope)

函式作用範圍(Function Scope)

如果你在一個函式內部宣告變數(使用 varletconst),那這個變數只會存在於這個函式中。

函式從 { 開始,到 } 結束,形成一個完整的封閉範圍。

function sayHi() {
  let message = "Hello!";
  console.log(message);
}

sayHi();              // → Hello!
console.log(message); // ❌ 錯誤,message 在這裡不存在

上面這段程式中,message 是函式 sayHi 的區域變數,它只存在於函式內部。

當你在函式外部嘗試使用 message,就會出現錯誤,因為它的範圍只限於函式裡。

這種情況叫做函式作用範圍(function scope),是早期 JavaScript 變數行為的主要範圍類型,特別是當你使用 var 宣告變數時。

區塊作用範圍(Block Scope)

除了函式,JavaScript 中的 {} 大括號也可以形成一個區塊範圍,像是:

  • if 判斷式
  • forwhile 迴圈
  • 任意以 {} 包起來的區塊

當你使用 letconst 宣告變數時,這些變數就只會存在於這對大括號 {} 裡面。

if (true) {
  let status = "logged in";
  console.log(status); // → logged in
}

console.log(status); // ❌ 錯誤,status 不存在於這裡

上面這個例子中,status 是用 let 宣告的,它只存在於 if 區塊內,出了這對 {} 就失效了,這就是所謂的區塊作用範圍(block scope)

小提醒:var 不遵守區塊範圍!

值得注意的是,var 雖然也可以在區塊中宣告變數,但它不會受到區塊範圍的限制,而是會跳出區塊、回到最近的函式或全域範圍。

這點和 letconst 非常不同,後面會再詳細介紹。

if (true) {
  var name = "Alice";
}

console.log(name); // → Alice ❗️雖然在 if 裡宣告,外面還是看得到

小結:函式範圍 vs 區塊範圍

如何形成用 function 宣告函式
有效的變數宣告方式var / let / const
範圍限制範例僅在整個函式內有效
如何形成用 {} 包起來的條件、迴圈、區塊等
有效的變數宣告方式let / const
範圍限制範例僅在 {} 中有效,外部無法存取

🚨 注意:var 不會被區塊範圍限制,只會被函式範圍限制。

理解這兩種「區域範圍」的差異,能幫助你更有信心控制變數的使用範圍,也能避免常見的錯誤,例如:變數意外被覆蓋、變數跑出預期範圍等問題。

下一段,我們就會進一步比較 varletconst 三者的差異,幫助你選擇最適合的宣告方式來寫出安全、穩定的 JavaScript 程式碼。

varletconst:三種宣告方式的作用範圍差異

在 JavaScript 中,宣告變數有三種方式:varletconst

雖然它們都能用來建立變數,但它們的作用範圍其實不一樣,這會直接影響你寫出來的程式是安全還是容易出錯。

var 的範圍比較寬鬆(不受區塊限制)

在 JavaScript 中,var 是最早期就存在的變數宣告方式,它和後來加入的 letconst 不一樣,有一個非常特別且容易出錯的行為

它沒有「區塊範圍」的概念,只有「函式範圍」與「全域範圍」。

也就是說,即使你在一個 {} 區塊裡使用 var 宣告變數,它也不會被侷限在這個區塊裡

這種現象常常會讓人以為變數「只存在那塊裡」,結果卻在外部還能使用它,導致意料之外的行為。

🔍 例子:var 不會被限制在 {} 區塊中

if (true) {
  var message = "Hello!";
  console.log("區塊內:", message); // → 區塊內:Hello!
}

console.log("區塊外:", message);    // → 區塊外:Hello! 😮

在這段程式碼中,我們在 if 區塊裡用 var 宣告了一個變數 message

直覺上,你可能會以為它應該只存在於 if 裡,但實際上它「跑出區塊」,在外面依然能用

這就是因為 var 不支援區塊作用範圍,只會看最近的「函式」或「全域」範圍。

如果這段程式碼寫在函式外部,那 message 就會直接變成全域變數。

🧠 這會帶來什麼問題?

這種範圍寬鬆的特性,看起來好像沒什麼,但實際上會造成很多潛在的錯誤,特別是:

你以為變數只在區塊內有效,其實它跑出來了

這可能讓你在區塊外不小心又使用到同一個變數名稱,結果資料被覆蓋,卻不知道是怎麼發生的。

程式可讀性變差,維護困難

當一個變數的範圍很大,你就很難判斷它是從哪裡來的、被誰改過、值是什麼時候變的。

🧪 再看一個例子:在 for 迴圈裡使用 var

for (var i = 0; i < 3; i++) {
  console.log("迴圈內:", i);
}

console.log("迴圈外:", i); // → 3 😬

你可能會以為 i 是迴圈裡的變數,應該只能在 for 裡使用。

但因為用了 vari 會「衝出」迴圈,在外面依然存在,而且值還是最後一次迴圈結束時的結果。

letconst 有真正的「區塊範圍」

在 JavaScript 的早期版本中,變數宣告只有 var 一種方式,它只能依附在「函式範圍」上,這會讓某些變數不小心跑到外面被使用,造成錯誤。

為了解決這個問題,JavaScript 在 ES6(2015年) 推出了兩個新的變數宣告方式:letconst

這兩者最大的特點之一就是——它們擁有真正的「區塊作用範圍(block scope)」

🧱 什麼是區塊範圍?

區塊(block)是指由一對大括號 {} 包起來的程式區段,像是:

  • if (...) { ... }
  • for (...) { ... }
  • while (...) { ... }
  • 或你自己用 {} 包起來的區塊

如果你用 letconst 在這些區塊裡宣告變數,這些變數就只會存在於該區塊內部,離開這對 {},變數就會「失效」。

📦 範例:letconst 限定在區塊內有效

if (true) {
  let greeting = "Hi!";
  const name = "Amy";
  console.log(greeting, name); // → Hi! Amy
}

console.log(greeting); // ❌ 錯誤:greeting is not defined
console.log(name);     // ❌ 錯誤:name is not defined

在這個例子中,我們在 if 區塊內宣告了 greetingname

  • greetinglet 宣告
  • nameconst 宣告

它們都只存在於 if{} 區塊內,一旦程式跑出這個區塊,再使用這兩個變數就會出錯,因為它們已經「離線」了。

🔐 為什麼這樣比較安全?

這種「區塊範圍」的設計是為了讓變數只在需要它的地方存在,其他地方都不能碰到它

這樣可以:

  1. ✅ 避免誤用變數(不小心在其他地方用到一樣的名稱)
  2. ✅ 增加程式可讀性(看到一個變數,就知道它只在這裡用)
  3. ✅ 降低錯誤風險(不會誤改變一個其他區塊的值)

letconst 有什麼不同?

let✅ 可以重新指定值
const❌ 一旦指定就不能再改(不可重新賦值)
let通常用來表示會改變的變數
const通常用來表示不會改變的常數或設定值
let區塊範圍(block scope)
const區塊範圍(block scope)
let count = 0;
count = 1; // ✅ OK

const pi = 3.14;
pi = 3.14159; // ❌ 錯誤,不能改值

但注意,const 是「不能改變變數的參考」,不代表值本身不會變
像是物件或陣列這種可變資料,雖然不能把整個變數改掉,但裡面的內容還是可以修改的(這部分是進階內容,可以之後再介紹)。

小結:為什麼推薦使用 letconst

區塊作用範圍(block scope)❌ 否
函式作用範圍(function scope)✅ 是
全域範圍可能外洩✅ 容易
是否建議使用❌ 不建議
區塊作用範圍(block scope)✅ 是
函式作用範圍(function scope)✅ 是
全域範圍可能外洩❌ 安全
是否建議使用✅ 推薦
區塊作用範圍(block scope)✅ 是
函式作用範圍(function scope)✅ 是
全域範圍可能外洩❌ 安全
是否建議使用✅ 推薦
  • 它們擁有明確的區塊範圍,不容易誤用
  • 它們的行為更符合人類直覺:「在 if 裡宣告,就只在 if 裡有效」
  • 它們能更清楚地表達你的意圖(是否允許修改)

因此,在現代 JavaScript 開發中,幾乎都使用 letconst,完全不再使用 var,除非你在維護舊版系統。

在理解 letconst 的區塊範圍之後,你的程式將會更有結構、更安全,也更容易閱讀與維護。

變數遮蔽

在了解了 varletconst 的作用範圍後,你現在應該清楚 JavaScript 中有不同的範圍規則,決定了變數「在哪裡有效、在哪裡無效」。

但實際上還有一種情況很常見:

當不同範圍中出現同樣名稱的變數時,JavaScript 到底會用哪一個?

想像一下,你在函式裡寫了一個變數名稱,結果發現同時存在兩個同名變數:

一個是在函式外層已經宣告的;另一個是函式內部才剛宣告的。

這時候 JavaScript 到底會用內部的,還是外部的呢?

答案是:
JavaScript 會選擇距離目前位置最近的那個變數,優先使用函式內部的變數。

這種情況就叫做 變數遮蔽(Shadowing),也就是內層的變數會「遮蔽」掉外層的變數,讓你只看見最近定義的那個。

簡單說:

  • 函式內有自己的變數或參數時,會優先使用它,不再理會外層的同名變數。
  • 只有當函式內部沒有同名變數時,才會往外層去找。

你可以把這個過程想像成「同名變數的距離比賽」:

JavaScript 會從你的使用位置開始往外層找,一旦找到第一個同名的變數,就直接使用這個變數,後面更遠的變數就不再考慮了。

什麼是「內部」與「外部」?

當我們說「JavaScript 會從函式自己內部開始找變數」時,其實是在說明一個作用範圍的搜尋順序

但這裡的「內部」與「外部」到底是什麼意思呢?我們來一步步拆解:

✅ 內部(Inner Scope):就是你現在寫程式的那個區塊

所謂的「內部」,指的是你當下所在的作用範圍,也可以理解為「離你最近的 {} 包起來的範圍」,通常是某個函式、if 區塊、for 迴圈等等。

舉個例子:

function greet() {
  let message = "Hi!";
  console.log(message); // 🟢 這裡就處於 greet 函式的「內部」
}

在這段程式碼中:

  • console.log(message) 就是在 greet() 函式的內部
  • 它會先從「自己這一層」找有沒有叫 message 的變數(有,就用它)

✅ 外部(Outer Scope):就是包住你的區塊的外一層

所謂「外部」,指的是包住你目前這層作用範圍的外面那一層

當你在「內層」找不到某個變數時,JavaScript 就會往「外層」繼續找。

讓我們來看個例子:

let name = "Amy";

function greet() {
  console.log("Hello, " + name); // 🔍 沒有 name?往外層找!
}

greet(); // → Hello, Amy

在這裡:

  • name 是在函式 greet() 的「外部」宣告的變數
  • 因為函式內沒有自己的 name,所以 JavaScript 就會往外找
  • 在外面找到一個 name = "Amy",就使用它

🔁 變數搜尋的順序(從內往外)

總結一下:

當你在某個作用範圍內用到變數時,JavaScript 會依照以下順序去找:

1️⃣ 先看自己裡面有沒有

2️⃣ 如果沒有,就往包住它的外層區塊

3️⃣ 再沒有,就繼續往更外層

4️⃣ 一直找,直到全域為止;如果整個程式都找不到,才會報錯

📦 綜合範例:內部 vs 外部

let name = "Alice"; // → 外部變數

function greet() {
  let greeting = "Hello"; // → 內部變數
  console.log(greeting + ", " + name + "!"); // → name 是往外找來的
}

greet(); // → Hello, Alice!

這裡的 greeting 是在函式內部定義的,所以直接使用

name 並沒有在函式內部出現過 → 所以往外層找 → 找到全域的 name = "Alice"

範例:函式參數遮蔽外部變數

const halve = function(n) {
  return n / 2;
};

let n = 10;
console.log(halve(100)); // → 50
console.log(n);          // → 10

這裡有兩個變數名稱都叫做 n

  • 一個是在函式 halve 裡面,是它的參數 n
  • 另一個是在函式外部,宣告為 let n = 10

當我們呼叫 halve(100) 時,這個 100 就會被傳進函式內部,作為 n 的值。

此時函式裡的 n 就是 100,而 JavaScript 只會用這個內部的 n完全不會去看外部的 n(即使它也存在)

因此 return n / 2 計算的是 100 / 2,輸出 50。外部的 n 完全沒有受到影響,它還是原本的 10

這就展現了「遮蔽」的效果:同名變數,只會使用最近定義的那一個

為什麼 JavaScript 要這樣設計?

這種「最近的變數優先使用」的設計,有幾個好處:

  1. 避免混淆與誤用:當你在函式裡寫變數時,你可以明確知道你用的是哪個變數,不會突然抓到外面意料之外的東西。
  2. 讓變數彼此獨立、不互相干擾:外層有外層的變數,內層有內層的變數,雖然名字相同,但互不影響。
  3. 提升程式可讀性與維護性:看到某個變數時,你可以很快知道它是從哪裡來的。

💡 額外提醒:遮蔽只是「擋住」而不是「改掉」

變數遮蔽並不會去改掉外部的變數,它只是讓你在裡面看不到它而已。

let user = "Alice";

function showUser() {
  let user = "Bob"; // 這裡的 user 遮蔽了外面的 user
  console.log(user); // → Bob
}

showUser();
console.log(user); // → Alice(外部的 user 不受影響)
  • 函式裡的 user 是自己的變數,值是 "Bob"
  • 它擋住了外面的 user(”Alice”),所以 console.log(user) 顯示的是 "Bob"
  • 但函式外的 user 仍然存在,值沒有被改變

這也是為什麼在設計變數時,最好避免在內外層使用同樣的名稱,除非你很清楚自己要的效果,否則容易混淆。

小結:變數遮蔽的三個重點

結果只會使用內部的變數,外部的會被遮住
結果程式會往外層繼續找
結果彼此獨立、互不影響

遮蔽是一個自然且預期的行為,它讓我們在程式中可以重複使用變數名稱而不擔心產生衝突,也讓每個變數更容易控制與追蹤。

巢狀作用範圍(Nested Scope)是什麼?

每一層都是一個獨立的區域(也就是一個作用範圍),彼此之間有明確的界線。

在 JavaScript 中,每當你宣告一個函式或區塊(例如 ifforwhile),就會創造出一個新的作用範圍

而你又可以在這些作用範圍裡繼續宣告新的函式或區塊,產生巢狀(nested)的關係。

簡單來說:內層可以讀取外層的變數,但外層看不到內層的東西。這就像一間辦公室裡面還有會議室,會議室的人可以看到外面的公告,但辦公室的人卻看不到會議桌上的筆記。

巢狀作用範圍範例:點餐系統

function takeOrder(customerName) {
  const orderNumber = 42;

  function printOrder() {
    const drink = "奶茶";
    console.log(`${customerName} 的訂單是 #${orderNumber}${drink}`);
  }

  printOrder();
}

takeOrder("小明");
// → 小明 的訂單是 #42:奶茶

我們來把這段程式碼分層解析一下:

🔸 第一層:外層函式 takeOrder

  • 定義了變數 customerName(參數)和 orderNumber
  • 這些變數只存在於 takeOrder 的範圍中
  • 同時,裡面還定義了一個內層函式 printOrder

🔸 第二層:內層函式 printOrder

  • 在這裡宣告了一個區域變數 drink
  • drink 只能在 printOrder() 裡被使用。
  • 但它可以使用外層的 customerNameorderNumber

巢狀作用範圍的基本原則

解釋printOrder() 裡可以讀取 customerName 和 orderNumber。
解釋takeOrder() 裡無法讀取 drink,因為它只存在於 printOrder() 中。
解釋即使變數名稱相同,它們也是不同的變數,互不干擾。

觀念補充:為什麼要有巢狀作用範圍?

巢狀作用範圍的設計,帶來三大好處:

  1. 變數更容易管理
    • 每層的變數都有自己清楚的範圍,讀程式的人更容易理解資料從哪來。
  1. 避免變數互相干擾
    • 如果沒有範圍限制,一個變數在別的地方被改掉就會出大問題。有了巢狀範圍,內外變數互不干擾,讓程式更穩定
  1. 促進程式的模組化設計
    • 每個函式就像一個小單位,可以擁有自己的「小世界」,只處理自己該處理的事,不需要知道外面所有的狀況

延伸練習(觀念測試)

如果我們這樣寫會怎樣?

function takeOrder(customerName) {
  function printOrder() {
    const drink = "奶茶";
  }

  console.log(drink); // ❌ 錯誤:找不到 drink
}

答:會報錯!因為 drink 是在 printOrder() 裡宣告的變數,takeOrder() 外層是看不到也存取不到的。

小結

巢狀作用範圍是 JavaScript 中非常基本又非常重要的觀念,它確保:

  • 變數使用的安全性和清晰性。
  • 每層函式或區塊都能有自己的「專屬變數」。
  • 內層可以讀外層,但外層永遠不能讀內層

👉 接下來我們會介紹 JavaScript 為什麼能做到這種「從內往外找變數」的行為,這背後的原理就叫做:詞彙作用範圍(Lexical Scope)

為什麼內層可以用外層變數?──詞彙作用範圍(Lexical Scope)

在剛剛的點餐系統範例中,我們看到一個很有趣的現象:

function takeOrder(customerName) {
  const orderNumber = 42;

  function printOrder() {
    const drink = "奶茶";
    console.log(`${customerName} 的訂單是 #${orderNumber}${drink}`);
  }

  printOrder();
}

👉 printOrder() 雖然是takeOrder() 裡面被定義出來的函式,但它卻可以直接使用 customerNameorderNumber 這兩個變數。

而且這兩個變數並不是在 printOrder() 裡面宣告的,卻好像早就屬於它一樣。

這到底是為什麼呢?

這是因為 JavaScript 會根據一套固定的規則,去一層一層地找變數。

這套規則就叫做:

🎯 詞彙作用範圍(Lexical Scope)

為什麼叫「詞彙作用範圍(Lexical Scope)」?

「詞彙(Lexical)」這個詞指的是「文字的結構」,所以「詞彙作用範圍」的意思就是:

🧩 根據「語法上的位置」來決定變數範圍的規則。

只要你看懂一段程式的結構長怎樣,你就能知道變數能在哪裡被使用──不用等到程式跑起來才能知道。這就是它的「靜態、可預測」的特性。

🚫 常見誤解:變數是等到執行時才決定作用範圍的?

不少人會以為:

「JavaScript 執行到某個函式時,才會決定當下該去哪裡找變數,
可能是從呼叫者的位置往上找,也可能是從目前狀態決定。」

這種想法看起來很合理,因為我們直覺會覺得:「程式是執行中的東西,變數當然是執行到哪裡用哪裡的變數啊?」

但這在 JavaScript(還有許多語言)中是錯的。

✅ 正確觀念:作用範圍在「寫程式當下」就決定好了

JavaScript 採用的是「詞彙作用範圍(Lexical Scope)」,也就是說:

函式寫在哪裡,就決定了它可以使用哪些變數。

這裡的「寫在哪裡」不是指你在程式執行時從哪裡呼叫,而是你在寫程式的時候,這個函式是被寫在誰的 {} 裡面

🏠 小比喻:詞彙作用範圍就像是建房子

你可以把「程式的作用範圍」想像成一間房子裡的房間結構。

  • 你在畫藍圖時(寫程式碼時),就已經決定好哪個房間放哪些東西(變數)。
  • 之後就算有人進出房間(執行函式),他們能看到的東西也不會變,因為東西放在哪裡早就決定好了。

🔑 重點是:你寫程式的方式(變數放在哪裡、函式定義在哪裡)才是決定範圍的關鍵,不是執行順序。

範例:誰包住誰的作用範圍

let teacher = "王老師";

function outer() {
  let teacher = "李老師";

  function inner() {
    console.log("現在的老師是:" + teacher);
  }

  inner();
}

outer();

🔍 執行結果

現在的老師是:李老師

🧠 解說:為什麼會這樣?

當我們執行 outer(),裡面的 inner() 也跟著被呼叫。但這時候 inner() 印出來的 teacher 是「李老師」,而不是「王老師」。

為什麼呢?關鍵在於:

👉 inner() 是寫在 outer() 裡面,它被 outer(){} 包住了。

這代表:

  • inner() 找變數時,會從自己所屬的區塊往外找
  • 它首先會看到 outer() 裡面的 let teacher = "李老師",就直接使用它了。
  • 不會再往更外層的 let teacher = "王老師" 去找。

這就是所謂的「詞彙作用範圍」:

👉 你寫下這段程式時,誰包住這個函式,它就能看到誰。

再看一個對照範例:呼叫位置 ≠ 決定範圍

let food = "壽司";

function eat() {
  console.log("今天吃:" + food);
}

function lunchTime() {
  let food = "拉麵";
  eat(); // 在這裡呼叫 eat()
}

lunchTime();

🔍 執行結果

今天吃:壽司

🧠 為什麼是「壽司」,不是「拉麵」?

因為 eat()寫在全域範圍裡,不在 lunchTime(){} 裡,它根本不知道 lunchTime() 有一個 food = "拉麵"

所以 eat() 找變數 food 時,只能從自己定義的位置往外找,看到的是全域的 food = "壽司"

最重要的觀念再講一次:

🧱 你寫下一個函式時,它的作用範圍就被「當時的外層程式區塊」決定了,不會因為之後在哪裡呼叫它而改變。

這種從內層往外層找變數、根據「寫程式的位置」來決定作用範圍的規則,就是 JavaScript 的詞彙作用範圍。

延伸詞彙作用範圍:從「函式也是值」到閉包(Closure)

我們前面學到一個很重要的觀念──詞彙作用範圍(Lexical Scope)

函式可以看到哪些變數,取決於它「被寫在哪裡」,而不是從哪裡呼叫的。

也就是說,如果你把一個函式寫在另一個函式裡面,那它就能看到外面函式裡定義的變數。

像這樣:

function outer() {
  let message = "Hello!";

  function inner() {
    console.log(message); // inner 看得到外層的 message
  }

  inner();
}

outer(); // → Hello!

這段程式碼非常直覺:

  • inner()outer() 裡面,所以能看到外面的變數 message

這就是我們前面提過的「詞彙作用範圍」。

但有一個更特別的情況是:

在 JavaScript 裡,函式本身也是一種值(value)

什麼意思呢?
它的意思是說,函式除了能直接執行之外,你還能:

  • 把函式儲存在變數裡面
  • 把函式當成參數傳給另一個函式
  • 把函式用 return 回傳出去給別人使用

舉個例子:

function getGreeting() {
  return "Hello!";
}

const greet = getGreeting(); // greet 現在是 "Hello!"
console.log(greet);          // → Hello!

這個例子很簡單,因為回傳的是字串。但 JavaScript 還能做更酷的事情──

它甚至可以直接把函式回傳出去

function getGreetingFunction() {
  return function() {
    console.log("Hello!");
  };
}

const greetFn = getGreetingFunction(); // greetFn 現在是一個函式
greetFn(); // → Hello!

這樣做的好處是,你可以讓函式不一定要馬上執行,而是可以交給別人,或等之後有需要時再使用它。

這裡就有一個有趣的狀況:

當你把函式從另一個函式裡回傳出去,而且這個回傳出去的函式使用到外層函式的變數,就會產生一個特別的效果:

這個被回傳出去的函式,會永遠記得那些外層的變數

我們直接看實際程式碼,你會更清楚:

function outer() {
  let message = "Hello!";

  return function() { 
    console.log(message);
  };
}

const sayHi = outer(); // outer() 回傳一個函式給 sayHi
sayHi();               // → Hello!

我們的程式發生了什麼事?

  • 執行 outer() 後,我們拿到了一個新的函式,放進 sayHi
  • 當我們後續執行 sayHi() 時,它居然還記得原本定義在 outer() 裡的變數 message

這種現象,就叫做 閉包(Closure)

「閉包」聽起來很奇怪,但其實它的核心概念很簡單:

📌 閉包就是一個函式,加上它被建立時「看到的那些外層變數」。

這樣的設計能讓函式帶著自己的環境到處走,而不會失去這些重要的資訊。

接下來,我們就會更詳細地透過程式範例,教你閉包的完整觀念與實際用途。

深入理解閉包:從計數器範例出發

剛才我們提到:

📌 閉包就是一個函式,加上它被建立時「看到的那些外層變數」。

但這個概念聽起來還是有點抽象。我們透過更實際的例子,來一步步拆解它。

範例:建立一個「計數器」

我們想要寫一個函式,每次呼叫它,都會幫我們計算執行次數。

首先,你可能想到這種最簡單的方式:

let count = 0; // 設一個變數記住次數

function add() {
  count += 1;  // 每次執行就增加 1
  return count;
}

console.log(add()); // → 1
console.log(add()); // → 2
console.log(add()); // → 3

但這樣有個問題:

  • 任何人都能直接更改 count 變數,可能會導致計算錯誤。
  • 我們更希望的是,每個計數器都有自己的獨立變數,不會互相干擾。

使用閉包建立安全的「計數器」

我們試著用閉包來改寫剛才的範例:

function createCounter() {
  let count = 0; // 這個變數是「私有的」

  return function() {
    count += 1;
    return count;
  };
}

const counterA = createCounter(); // 建立第一個計數器
const counterB = createCounter(); // 建立第二個計數器(獨立於A)

console.log(counterA()); // → 1
console.log(counterA()); // → 2
console.log(counterA()); // → 3

console.log(counterB()); // → 1(注意這裡又從1開始)
console.log(counterB()); // → 2

你會發現一件很有趣的事:

  • counterAcounterB 都是獨立運作的計數器。
  • 每個計數器都有自己的 count彼此不互相影響
  • 這個變數 count別人無法從外部存取的

為什麼能辦到呢?因為這裡就運用了 閉包

🔍 這裡到底發生了什麼事?

讓我們一步步解釋:

  1. 當你呼叫 createCounter() 時,它裡面建立了一個變數 count,並設為 0。
  2. 然後,createCounter() 回傳了一個新的函式給你。這個函式會用到前面建立的變數 count
  3. JavaScript 這時候發現一件事:

💡「這個回傳出去的函式,以後還會用到外面的變數 count 耶!」

因此,JavaScript 會自動把這個 count 變數保留下來,即使原本的函式(createCounter)早就執行完畢了。

  1. 這樣一來,當你未來再呼叫這個被回傳出去的函式(例如 counterA())時,變數 count 依然可以被使用,並且維持原來的值。

這整個機制,就是所謂的「閉包」:

函式(這裡是回傳出去的函式)會自動保留它出生時看到的變數,即使原本的函式執行完畢了,這些變數依然還存在。

閉包的特性整理

我們再簡單整理一下:

說明閉包經常發生在函式回傳函式的情境中
說明閉包會保留這些外層變數,不讓它們消失
說明因為回傳的函式需要這些變數

💡 閉包能解決哪些問題?

  • 保護變數安全:建立私有的變數,外界無法修改
  • 維持函式的內部狀態:例如前面的計數器、計時器等等
  • 建立獨立的函式實例:每個函式實例都有自己的內部變數,互不干擾

閉包讓 JavaScript 變得更強大且更安全,這也是為什麼它這麼重要。

結語

搞懂作用範圍是成為 JavaScript 開發者的第一步。無論是函式、區塊或巢狀結構,清楚知道變數在哪裡有效、哪裡無效,能幫助你寫出更乾淨且無錯誤的程式碼

記住幾個關鍵點:

  • 使用 letconst 建立區塊範圍變數。
  • var 是舊式寫法,容易造成作用範圍混亂。
  • 函式內部看不到外部以外的變數,除非傳入。
  • 相同變數名稱,會使用「最近」那個。

透過這些觀念,你的 JavaScript 寫作能力將更加穩固!