在學習 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):只在特定範圍內有效的變數
和全域變數相反,區域變數是只在特定範圍內有效的變數。
這個範圍可以是兩種常見情況之一:
- 一個「函式」的範圍(function scope)
- 一對大括號
{}組成的「區塊」範圍(block scope)
函式作用範圍(Function Scope)
如果你在一個函式內部宣告變數(使用 var、let 或 const),那這個變數只會存在於這個函式中。
函式從 { 開始,到 } 結束,形成一個完整的封閉範圍。
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判斷式for、while迴圈- 任意以
{}包起來的區塊
當你使用 let 或 const 宣告變數時,這些變數就只會存在於這對大括號 {} 裡面。
if (true) {
let status = "logged in";
console.log(status); // → logged in
}
console.log(status); // ❌ 錯誤,status 不存在於這裡上面這個例子中,status 是用 let 宣告的,它只存在於 if 區塊內,出了這對 {} 就失效了,這就是所謂的區塊作用範圍(block scope)。
小提醒:var 不遵守區塊範圍!
值得注意的是,var 雖然也可以在區塊中宣告變數,但它不會受到區塊範圍的限制,而是會跳出區塊、回到最近的函式或全域範圍。
這點和 let、const 非常不同,後面會再詳細介紹。
if (true) {
var name = "Alice";
}
console.log(name); // → Alice ❗️雖然在 if 裡宣告,外面還是看得到小結:函式範圍 vs 區塊範圍
| 範圍類型 | 如何形成 | 有效的變數宣告方式 | 範圍限制範例 |
|---|---|---|---|
| 函式作用範圍 | 用 function 宣告函式 | var / let / const | 僅在整個函式內有效 |
| 區塊作用範圍 | 用 {} 包起來的條件、迴圈、區塊等 | let / const | 僅在 {} 中有效,外部無法存取 |
🚨 注意:
var不會被區塊範圍限制,只會被函式範圍限制。
理解這兩種「區域範圍」的差異,能幫助你更有信心控制變數的使用範圍,也能避免常見的錯誤,例如:變數意外被覆蓋、變數跑出預期範圍等問題。
下一段,我們就會進一步比較 var、let、const 三者的差異,幫助你選擇最適合的宣告方式來寫出安全、穩定的 JavaScript 程式碼。
var、let、const:三種宣告方式的作用範圍差異
在 JavaScript 中,宣告變數有三種方式:var、let 和 const。
雖然它們都能用來建立變數,但它們的作用範圍其實不一樣,這會直接影響你寫出來的程式是安全還是容易出錯。
var 的範圍比較寬鬆(不受區塊限制)
在 JavaScript 中,var 是最早期就存在的變數宣告方式,它和後來加入的 let、const 不一樣,有一個非常特別且容易出錯的行為:
它沒有「區塊範圍」的概念,只有「函式範圍」與「全域範圍」。
也就是說,即使你在一個 {} 區塊裡使用 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 裡使用。
但因為用了 var,i 會「衝出」迴圈,在外面依然存在,而且值還是最後一次迴圈結束時的結果。
let 和 const 有真正的「區塊範圍」
在 JavaScript 的早期版本中,變數宣告只有 var 一種方式,它只能依附在「函式範圍」上,這會讓某些變數不小心跑到外面被使用,造成錯誤。
為了解決這個問題,JavaScript 在 ES6(2015年) 推出了兩個新的變數宣告方式:let 和 const。
這兩者最大的特點之一就是——它們擁有真正的「區塊作用範圍(block scope)」。
🧱 什麼是區塊範圍?
區塊(block)是指由一對大括號 {} 包起來的程式區段,像是:
if (...) { ... }for (...) { ... }while (...) { ... }- 或你自己用
{}包起來的區塊
如果你用 let 或 const 在這些區塊裡宣告變數,這些變數就只會存在於該區塊內部,離開這對 {},變數就會「失效」。
📦 範例:let 和 const 限定在區塊內有效
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 區塊內宣告了 greeting 和 name:
greeting用let宣告name用const宣告
它們都只存在於 if 的 {} 區塊內,一旦程式跑出這個區塊,再使用這兩個變數就會出錯,因為它們已經「離線」了。
🔐 為什麼這樣比較安全?
這種「區塊範圍」的設計是為了讓變數只在需要它的地方存在,其他地方都不能碰到它。
這樣可以:
- ✅ 避免誤用變數(不小心在其他地方用到一樣的名稱)
- ✅ 增加程式可讀性(看到一個變數,就知道它只在這裡用)
- ✅ 降低錯誤風險(不會誤改變一個其他區塊的值)
❓ let 和 const 有什麼不同?
| 項目 | let | const |
|---|---|---|
| 是否能改值 | ✅ 可以重新指定值 | ❌ 一旦指定就不能再改(不可重新賦值) |
| 使用情境 | 通常用來表示會改變的變數 | 通常用來表示不會改變的常數或設定值 |
| 作用範圍 | 區塊範圍(block scope) | 區塊範圍(block scope) |
let count = 0;
count = 1; // ✅ OK
const pi = 3.14;
pi = 3.14159; // ❌ 錯誤,不能改值但注意,const 是「不能改變變數的參考」,不代表值本身不會變。
像是物件或陣列這種可變資料,雖然不能把整個變數改掉,但裡面的內容還是可以修改的(這部分是進階內容,可以之後再介紹)。
小結:為什麼推薦使用 let 和 const?
| 宣告方式 | 區塊作用範圍(block scope) | 函式作用範圍(function scope) | 全域範圍可能外洩 | 是否建議使用 |
|---|---|---|---|---|
var | ❌ 否 | ✅ 是 | ✅ 容易 | ❌ 不建議 |
let | ✅ 是 | ✅ 是 | ❌ 安全 | ✅ 推薦 |
const | ✅ 是 | ✅ 是 | ❌ 安全 | ✅ 推薦 |
- 它們擁有明確的區塊範圍,不容易誤用
- 它們的行為更符合人類直覺:「在 if 裡宣告,就只在 if 裡有效」
- 它們能更清楚地表達你的意圖(是否允許修改)
因此,在現代 JavaScript 開發中,幾乎都使用 let 和 const,完全不再使用 var,除非你在維護舊版系統。
在理解 let 和 const 的區塊範圍之後,你的程式將會更有結構、更安全,也更容易閱讀與維護。
變數遮蔽
在了解了 var、let、const 的作用範圍後,你現在應該清楚 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 要這樣設計?
這種「最近的變數優先使用」的設計,有幾個好處:
- ✅ 避免混淆與誤用:當你在函式裡寫變數時,你可以明確知道你用的是哪個變數,不會突然抓到外面意料之外的東西。
- ✅ 讓變數彼此獨立、不互相干擾:外層有外層的變數,內層有內層的變數,雖然名字相同,但互不影響。
- ✅ 提升程式可讀性與維護性:看到某個變數時,你可以很快知道它是從哪裡來的。
💡 額外提醒:遮蔽只是「擋住」而不是「改掉」
變數遮蔽並不會去改掉外部的變數,它只是讓你在裡面看不到它而已。
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 中,每當你宣告一個函式或區塊(例如 if、for、while),就會創造出一個新的作用範圍。
而你又可以在這些作用範圍裡繼續宣告新的函式或區塊,產生巢狀(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()裡被使用。- 但它可以使用外層的
customerName和orderNumber。
巢狀作用範圍的基本原則
| 原則 | 解釋 |
|---|---|
| 內層看得到外層 | printOrder() 裡可以讀取 customerName 和 orderNumber。 |
| 外層看不到內層 | takeOrder() 裡無法讀取 drink,因為它只存在於 printOrder() 中。 |
| 每一層作用範圍都是獨立的 | 即使變數名稱相同,它們也是不同的變數,互不干擾。 |
觀念補充:為什麼要有巢狀作用範圍?
巢狀作用範圍的設計,帶來三大好處:
- ✅ 變數更容易管理
- 每層的變數都有自己清楚的範圍,讀程式的人更容易理解資料從哪來。
- ✅ 避免變數互相干擾
- 如果沒有範圍限制,一個變數在別的地方被改掉就會出大問題。有了巢狀範圍,內外變數互不干擾,讓程式更穩定。
- ✅ 促進程式的模組化設計
- 每個函式就像一個小單位,可以擁有自己的「小世界」,只處理自己該處理的事,不需要知道外面所有的狀況。
延伸練習(觀念測試)
如果我們這樣寫會怎樣?
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() 裡面被定義出來的函式,但它卻可以直接使用 customerName 和 orderNumber 這兩個變數。
而且這兩個變數並不是在 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你會發現一件很有趣的事:
counterA和counterB都是獨立運作的計數器。- 每個計數器都有自己的
count,彼此不互相影響。 - 這個變數
count是別人無法從外部存取的。
為什麼能辦到呢?因為這裡就運用了 閉包!
🔍 這裡到底發生了什麼事?
讓我們一步步解釋:
- 當你呼叫
createCounter()時,它裡面建立了一個變數count,並設為 0。 - 然後,
createCounter()回傳了一個新的函式給你。這個函式會用到前面建立的變數count。 - JavaScript 這時候發現一件事:
💡「這個回傳出去的函式,以後還會用到外面的變數
count耶!」
因此,JavaScript 會自動把這個 count 變數保留下來,即使原本的函式(createCounter)早就執行完畢了。
- 這樣一來,當你未來再呼叫這個被回傳出去的函式(例如
counterA())時,變數count依然可以被使用,並且維持原來的值。
這整個機制,就是所謂的「閉包」:
✅ 函式(這裡是回傳出去的函式)會自動保留它出生時看到的變數,即使原本的函式執行完畢了,這些變數依然還存在。
閉包的特性整理
我們再簡單整理一下:
| 特性 | 說明 |
|---|---|
| 函式內回傳另一個函式 | 閉包經常發生在函式回傳函式的情境中 |
| 內層函式使用外層變數 | 閉包會保留這些外層變數,不讓它們消失 |
| 外層函式結束後變數仍存在 | 因為回傳的函式需要這些變數 |
💡 閉包能解決哪些問題?
- 保護變數安全:建立私有的變數,外界無法修改
- 維持函式的內部狀態:例如前面的計數器、計時器等等
- 建立獨立的函式實例:每個函式實例都有自己的內部變數,互不干擾
閉包讓 JavaScript 變得更強大且更安全,這也是為什麼它這麼重要。
結語
搞懂作用範圍是成為 JavaScript 開發者的第一步。無論是函式、區塊或巢狀結構,清楚知道變數在哪裡有效、哪裡無效,能幫助你寫出更乾淨且無錯誤的程式碼。
記住幾個關鍵點:
- 使用
let和const建立區塊範圍變數。 var是舊式寫法,容易造成作用範圍混亂。- 函式內部看不到外部以外的變數,除非傳入。
- 相同變數名稱,會使用「最近」那個。
透過這些觀念,你的 JavaScript 寫作能力將更加穩固!