在學習 JavaScript 的過程中,你一定會聽到「閉包(Closure)」這個詞。
它是理解 JavaScript 函式行為的重要觀念,尤其與變數的「作用範圍(Scope)」密切相關。
初學者常常會對閉包感到困惑,不過別擔心,這篇文章會透過具體範例,一步一步帶你了解什麼是閉包、它怎麼運作,以及我們可以怎麼應用這個特性來寫出更強大、更靈活的程式。
一個簡單又有趣的問題:變數去哪了?
資料和名字是怎麼連在一起的?
在寫 JavaScript 程式的時候,我們常常會先「準備一份資料」,然後幫它貼上一張標籤。
這張標籤就是變數的名字。
這樣以後我們要使用那份資料,只要叫出這張標籤的名字,JavaScript 就知道你要的是哪一份。
舉個簡單的例子:
let name = '小明';
console.log('哈囉,' + name);這裡我們做的事情是:
- 把「小明」這個資料貼上名叫
name的標籤 - 接著在
console.log裡,只要呼叫name,JavaScript 就會幫我們找到那張貼有標籤的資料,組合出完整的句子
這就像我們對 JavaScript 說:「name 是哪位?」它就會回你:「是『小明』!」
👍 為什麼這麼做?
因為資料可能會很長、很複雜、會重複使用,所以幫它貼標籤之後,我們就不用每次都重寫原本的內容。
這讓程式:
- 更簡潔
- 更容易讀懂
- 更方便日後修改
🔄 而且標籤可以重新貼上新資料
let name = '小明';
console.log(name); // 小明
name = '小美';
console.log(name); // 小美你可以把舊標籤撕掉,貼到新的資料上。
name 這個名字,原本代表「小明」,現在變成代表「小美」。
如果我們把這段程式包進一個函式裡呢?
我們試著加上一個函式,變成這樣:
function sayHello() {
let name = '小明';
console.log('哈囉,' + name);
}看起來和剛剛差不多,只是現在這個綁定(name = '小明')是寫在函式裡。
當我們呼叫這個函式時:
sayHello(); // ✅ 會印出:哈囉,小明一切照常沒問題。
但如果我們想在函式外面直接使用 name 呢?
console.log(name); // ❌ 會出錯:找不到 name🤔 為什麼會這樣?
在這裡,我們把「小明」這個資料綁在一個名字 name 上。
不過這個名字是寫在 sayHello 這個函式裡的,也就是說,這張「name 的標籤」只存在函式裡,外面是看不到的。
但為什麼?資料不是就在那裡嗎?
🧳 想像一下工作證的情境
你可以把程式想像成一棟公司大樓。這棟大樓裡有很多空間,比如會議室、儲藏室、後台…
不同的人拿著不同的工作證,就能進入不同的區域。
sayHello 就像是其中一個內部工作區。而變數 name 就像是貼在牆上的一張資料卡,上面寫著「小明」。
這時候:
- 如果你是公司員工,有工作證,可以進入工作區,自然就能看到牆上的資料。
- 但如果你是普通訪客,沒有工作證,你就只能在大廳活動,無法進到裡面,也看不到裡面寫了什麼。
🧪 所以你在外面呼叫 name,會發生什麼?
sayHello(); // ✅ 印出:哈囉,小明
console.log(name); // ❌ 錯誤:找不到 namename 是貼在「內部工作區」的資料,外面的訪客(也就是整個程式的其他部分)是沒資格看到的。
這不是因為資料不見了,而是你沒有權限看見它。
🎯 為什麼要這樣設計?
這種「有權限才能看資料」的設計,其實是為了讓程式更有秩序。
想像一下,如果公司把所有內部文件都貼在大廳,任何人都能看、能改,會發生什麼事?
- 訪客可能誤改資料
- 不同部門的人可能互相干擾
- 系統變得很難維護,容易出錯
所以,JavaScript 會自動幫你管好這些「變數的可見範圍」:
在哪裡寫的變數,只能在那個地方用;其他地方就算知道名字,也沒權限使用。
延伸閱讀:JavaScript 入門必學觀念:深入理解變數的作用範圍 (Scope)
想把資料「帶出來」,得靠函式幫忙
剛剛我們說過,變數像是公司內部的資料,只有拿到工作證、站在某個區域裡的人才能看到。
那現在我們想做一件事:
我可不可以在那個區域裡,偷偷寫下一份資料備份,交給一個可以帶出門的人?
等他離開之後,我還能透過他,拿到那份本來應該「看不到」的資料?
這聽起來像是在突破規則對吧?
但在 JavaScript 裡,這不是違規,而是一種設計好的能力,它就是「閉包」(closure)。
不過在我們真正開始範例前,有兩個重要的概念要先認識清楚 👇
函式值(Function Value):函式也能當資料用
在 JavaScript 裡,函式不只是「寫好就會執行」的東西。
它也可以像文字、數字一樣,當作一份「值」來使用,這叫做「函數值」。
舉例來說,你可以這樣寫:
let greet = function () {
console.log('哈囉');
};這代表你把一個函式,綁在變數 greet 上。
這個變數就像拿著一張標籤,上面貼的是「我知道怎麼打招呼」。
這時候你可以這樣使用它:
greet(); // → 印出:哈囉✅ 函式值的重點就是:函式也可以被存起來、當參數傳來傳去,甚至當成 return 的結果。
延伸閱讀:JavaScript 函式值教學:從定義、使用到實戰應用
函式也可以「回傳」另一個函式
在 JavaScript 中,不只可以把函式存進變數,還可以把一個函式「從另一個函式裡回傳」出去。
這聽起來可能有點特別,但其實很像把一位員工訓練好,然後從某個部門「派遣」到外面去工作。
來看這個例子:
function outer() {
let say = function () {
console.log('我是被回傳的函式');
};
return say;
}
let action = outer();
action(); // → 印出:我是被回傳的函式下面我們逐行解析這段程式到底在做什麼:
function outer() {這是宣告一個函式,名字叫做 outer。
它本身不會馬上執行,只是先準備好一套「之後可以執行的指令內容」。
let say = function () {
console.log('我是被回傳的函式');
};在 outer 函式裡面,我們宣告了一個變數 say。
這個變數綁定的是一個「函式值」,也就是說:這不是一個文字、數字,而是一段能被執行的程式碼。
這段函式的功能很簡單:只要執行,就會在畫面上印出:
我是被回傳的函式此時 say 變數,就像是一張貼著標籤「打招呼」的員工證,裡面記住了這段功能。
return say;
}這一行的意思是:把 say 這個函式值「回傳」出去。
也就是說,當有人呼叫 outer() 時,它不會直接印東西。
它會把 say 這位「準備好要打招呼的員工」送給你,由你決定要不要用、什麼時候用。
let action = outer();這一行才是實際執行 outer()。
- 我們呼叫了
outer(); - 它內部建立了
say這個函式; - 然後把
say回傳出來; - 最後我們把這個回傳回來的函式,存進變數
action裡。
所以現在,action 就等於 say。
換句話說:action 是一個能夠被執行的函式,它知道自己該印出那句話。
action(); // → 印出:我是被回傳的函式這一行就是實際執行 action 這個函式,也就是執行 say()。
所以畫面上就會印出這一行文字:
我是被回傳的函式這段程式告訴我們一個重要的觀念:
函式不只是用來「執行」,它也可以「回傳另一個函式」,
然後你可以把那個回傳的函式存起來,想用的時候再叫出來執行。
這就是我們之前說的「函數值」的威力。
就像你在部門內訓練了一位員工(say),但不是當下就讓他上場,而是把他交給另一個單位(action)慢慢用。
這樣的設計,讓我們可以一步一步建立出有記憶、有功能的「函式小工具」,
也就是下一步我們會介紹的「閉包」如何發揮威力的基礎。
範例一:用函式「包住」一個變數
先來看程式碼:
function wrapValue(n) {
let local = n;
let getValue = function () {
return local;
};
return getValue;
}逐行解析這段程式在做什麼
function wrapValue(n) {我們定義了一個函式 wrapValue,可以傳入一個數字 n。
let local = n;把傳進來的數字存到變數 local 裡。
這個變數被宣告在 wrapValue 函式的裡面,是一個區域變數,所以它的可使用範圍只限於這個函式的內部,執行完就該消失。
你可以把這想成:
當某位員工(也就是這次呼叫函式的人)進入一個限制區時,系統會幫他準備一份「只屬於他這次工作的私人資料表」。
這份資料表會貼上限制使用的標籤:「限內部人員存取」。
這份資料只有進來這個區域的人能用。
而且每次進來的人,都會拿到一份「只給你用」的副本。
離開之後,這份資料理論上就會被銷毀、收回。
let getValue = function () {
return local;
};這裡我們不是像之前那樣,用 function getValue() {} 的方式直接命名函式。
而是定義了一個「匿名函式」(也就是沒有名字的函式),然後把這個函式「指定」給變數 getValue。
這個匿名函式的工作很簡單:回傳 local 這個變數的值。
這個 getValue 是「函數值」,我們沒有馬上執行它,而是先存起來。
而因為它是在 wrapValue 裡面被創造出來的,它就有權限可以使用 local,這是關鍵!
return getValue;這一行的意思是:我們把變數 getValue 裡存著的那個函數值回傳出去。
注意,這裡我們沒有執行那個函式,只是把它當作資料一樣「傳出去」。
換句話說:
我們把一個「知道怎麼取得
local」的小工具函式交給外面的世界,讓你以後隨時可以拿來用。
而因為這個函式是在 wrapValue 的內部被建立出來的,它就擁有「這個函式區塊的內部權限」——也就是說,它可以合法存取 local。
這時候,閉包就形成了:
- 原本
local是一個區域變數,只能在wrapValue裡存取; - 但我們把一個內部出生、有權限存取
local的函式帶了出去; - JavaScript 發現這個被帶出去的函式還用得到
local,就會選擇「把那份資料保留下來」,讓函式在外面也能繼續使用它。
✅ 這就是閉包發生的時刻:當你把一個用到內部變數的函式傳出來,那個變數就不會被銷毀,而是跟著這個函式一起存活下去。
接著這樣使用函式:
let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1()); // → 1
console.log(wrap2()); // → 2發生了什麼事?逐行解析來看:
第一步:呼叫 wrapValue(1)
let wrap1 = wrapValue(1);這一行的意思是:
- 我們呼叫
wrapValue(1),傳進數字1作為參數; - 進入
wrapValue裡,JavaScript 幫我們建立了一個新的資料空間,這裡面有個變數local = 1; - 接著,程式定義了一個函式
getValue,裡面寫著「當我被呼叫時,請回傳local的值」; - 然後,把這個
getValue函式「回傳到外面」,交給外部程式保存; - 外面的程式把這個函式值存進變數
wrap1裡。
此時的 wrap1 是什麼?
是一個記得 local = 1 的小函式。
這個函式不但還活著,還擁有內部那份資料的「存取權限」!
第二步:呼叫 wrapValue(2)
let wrap2 = wrapValue(2);這一行是跟剛剛同樣的流程,只是這次我們傳進 2:
- 再次呼叫
wrapValue(2),建立另一份資料空間,這次的local = 2; - 同樣建立一個新的
getValue函式,會回傳這個新的local; - 把這個函式傳回外面,存進
wrap2。
注意!這裡雖然程式碼長得一模一樣,但:
- 每一次呼叫
wrapValue(),都會產生全新的local和全新的getValue函式; wrap1和wrap2雖然來自同一個模版,但他們各自記得的是不同的資料,互不干擾!
第三步:執行這些函式
console.log(wrap1()); // → 1
console.log(wrap2()); // → 2這兩行分別在做什麼?
wrap1()呼叫的是來自wrapValue(1)的函式,它記得的是local = 1,所以回傳 1;wrap2()呼叫的是來自wrapValue(2)的函式,它記得的是local = 2,所以回傳 2。
換個說法來幫助理解
我們可以這樣想像這段程式:
let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);每次呼叫 wrapValue(n),就像是有一位員工走進一個「限制區」去執行一個任務。
進來的那一刻,系統會根據他帶進來的數字(例如 1 或 2),幫他準備一份專屬的私人資料表,上面寫著 local = 1 或 local = 2。
而這份資料表會標記:「限內部人員存取」,也就是只有在這次執行期間才能看見。
但這時有個特例:
我們在區域裡,還製作了一個小幫手 getValue,這個小幫手知道怎麼去查那張資料表。
當我們把這位小幫手傳出限制區(也就是 return 出來)時,他同時被授予了特殊通行權限,能繼續存取那張原本該被銷毀的資料表。
這樣的組合,我們就叫它——閉包(closure)。
所以當你寫下:
let wrap1 = wrapValue(1);就等於是你讓某位員工進去區域,幫他開好一張 local = 1 的資料表,然後把看得懂這張表的小幫手派出來,存進 wrap1。
下一次你寫下:
let wrap2 = wrapValue(2);同樣的流程,但這次的資料表寫的是 local = 2,而 wrap2 裡的小幫手就記得這一份。
這些小幫手雖然都離開了限制區,但他們身上都帶著特殊識別證,仍能安全地讀取原本屬於他們那一份的資料。
這背後代表什麼?
這段程式最重要的觀念是:
函式不只是拿來執行任務,它還能封裝資料並保留權限,讓某些資料能延續存在,甚至在外面被安全地使用。
這就是閉包的核心精神:保留內部資料的使用權,延伸變數的壽命,卻又不讓資料外洩。
接下來我們就來試試看,把這種能力應用到乘法器 multiplier的例子吧 🚀。
範例二:產生一個可以乘以任意數的函式
這次我們用一個更簡潔的寫法,示範閉包的另一個應用場景:
function multiplier(factor) {
return number => number * factor;
}先釐清幾個觀念
🔹 函式的參數,其實就是「內建的區域變數」
我們之前提過,只要某個變數是寫在函式裡的,它就被稱為區域變數(local variable)——這代表:
它的作用範圍只限在這個函式的「內部空間」,外面是完全看不到的。
就像你走進一個限制區,系統會幫你開一份只屬於你這次任務的資料表,這份表上會標明:「限本區人員使用」,其他人(包括你走出去之後的你)都不能再存取這份資料。
這就是為什麼我們說「區域變數會在函式執行完就銷毀」——因為你離開限制區後,資料就被系統回收了。
這邊還有一個很重要但容易忽略的細節:
在 JavaScript 裡,只要你宣告了一個函式,它的每個參數(像是 name、factor、number 這些)就會自動變成這個函式的「內部變數」——也就是區域變數。
你不需要額外用 let 或 const 去宣告它,它就已經存在了,而且:
它的使用範圍只限在這個函式裡面,外面是完全看不到、也用不到的。
👀 來看個例子:
function sayHello(name) {
console.log('哈囉,' + name);
}
sayHello('小明');
console.log(name); // ❌ 錯誤,name is not defined這裡的 name 是 sayHello 的一個參數,但你會發現:
- 在
sayHello裡面可以正常使用name; - 可是一旦離開函式、在外面直接使用
name,就會報錯說「找不到這個變數」。
這代表:不管你是用 let 宣告的區域變數,還是函式的參數,它們本質上都屬於那場「私人任務」中的專屬資料表,離開那場任務後就不再存在。
所以你可以把這種參數想像成:
這是系統在「函式開始時」自動幫你準備好的一份私人便條紙,每次呼叫函式時,它都會開一張新的,專屬於當次使用,外人無法存取。
這也是為什麼我們說:
參數其實就是一種「不用你自己宣告」的區域變數。
🔹 number => number * factor 是什麼東西?
這個寫法是 JavaScript 裡的箭頭函式(arrow function)語法。
簡單來說,它是匿名函式的縮寫寫法,也就是:
(number) => number * factor等同於這樣:
function (number) {
return number * factor;
}它是個沒有名字的函式(anonymous function),你可以把它想成:
一段臨時寫的小程式,只用一次,沒有必要幫它取名字。
🧠 舉個生活例子:
- 一般函式像是你辦公室裡的員工,有名牌、有職位。
- 匿名函式就像是臨時工,來幫忙一下,不需要登記名冊,做完工作就離開。
而箭頭函式只是把這種臨時工的寫法簡化成一行,變得更俐落:
// 傳統寫法(匿名函式)
return function (number) {
return number * factor;
};
// 箭頭函式寫法(更短)
return number => number * factor;注意:箭頭函式常用來「回傳一個簡單動作」,例如數學運算或字串處理,語法簡潔、易讀,是現代 JavaScript 中很常見的寫法。
當然可以,以下是這段內容的加強版改寫,我會幫你補上語意更清晰的描述、拆解每個步驟的原因,並延續你之前的權限/工作證比喻,讓讀者能更直覺地理解「閉包的使用」到底在做什麼:
開始使用這個閉包
來看看這段程式:
let twice = multiplier(2);
console.log(twice(5)); // → 10這看起來像是把某個數字乘以 2,但實際上,這整段背後運作的,是 JavaScript 閉包的機制。
讓我們一步一步拆解這個流程,看看裡面發生了什麼事:
🧩 第一步:呼叫 multiplier(2)
當我們執行這行:
let twice = multiplier(2);會發生以下幾件事:
- JavaScript 進入
multiplier函式的內部。 - 這時
factor = 2,這是一個區域變數,只在這次的multiplier呼叫中有效。 - 接著,函式回傳一個 匿名函式:
function (number) {
return number * factor;
}(這只是簡化語法的說法,實際是箭頭函式 number => number * factor)
- 雖然
multiplier()呼叫完成了,但這個被回傳出去的匿名函式,帶著對當時factor = 2的記憶被交給了外面的twice。
你可以這樣想:我們叫
multiplier這位部門主管幫我們生出一個員工twice,這位員工身上配有一張專屬工作證,上面寫著:「我可以查閱 factor = 2」。即使他之後不再待在原本部門內,這份權限依然有效。
🧩 第二步:呼叫 twice(5)
接著執行這行:
console.log(twice(5)); // → 10這其實就是在執行剛剛回傳的匿名函式。流程如下:
- 我們將數字
5傳進twice(),這時匿名函式裡的參數number = 5。 - 函式裡執行的內容是:
return number * factor;→ number 是現在傳進來的 5,
→ factor 則是早在第一次呼叫 multiplier(2) 時就記下來的 2(由閉包機制保存下來)。
- 所以最終計算是:
5 * 2,得到結果10。
換句話說:即使
factor這個變數早就離開multiplier的範圍,因為我們透過閉包把它「打包帶出來」,現在仍然可以使用它。
wrapValue 和 multiplier 的比較
我們剛剛看了兩個範例,它們其實都在做同一件事:
建立一個「有權限存取某個變數」的函式,然後把它傳到外面繼續使用。
但這兩個範例的寫法風格不太一樣。
wrapValue 的寫法比較「明確」
function wrapValue(n) {
let local = n;
let getValue = function () {
return local;
};
return getValue;
}這裡我們做了三件事:
- 用
let宣告了一個變數local,這是這次呼叫專屬的「區域資料表」; - 寫了一個匿名函式,這段程式會讀取
local,並把它指定給變數getValue; - 把這個
getValue函數值回傳出去,讓外部的程式可以透過它來查資料。
也就是說,我們不是把資料直接交出去,而是「派出一個知道怎麼查資料的代理人」。
這個代理人(函數值)來自限制區內部,擁有合法工作證,所以能夠存取 local。
multiplier 的寫法更「簡潔」
function multiplier(factor) {
return number => number * factor;
}這個版本看起來簡短很多,實際上做的事情是一樣的。
你可以這樣理解:
- 傳進來的
factor,其實也是一份「內部資料表」; - 我們沒有額外宣告變數,而是直接寫一個箭頭函式(匿名函式的縮寫),讓它記住
factor; - 然後把這個函式回傳出去,用來乘以其他數字。
這邊就沒用變數包裝,而是一步到位就把會用到內部變數的函式交出去。
兩者的差別比喻一下
你可以把 wrapValue 想成:
先建資料 → 指派代理人 → 把代理人派出去
而 multiplier 則像是:
當場建好代理人,順手就送出去了
兩者結果是一樣的,都是「產生一個擁有特定內部資料權限的函式值」,只是寫法的風格不同。
最重要的觀念
無論你是像 wrapValue 那樣分工明確,還是像 multiplier 那樣簡化寫法,它們都在證明一件事:
函式在被回傳出去時,會「帶著」它出生當下的變數環境(也就是閉包)。
這就是閉包的核心能力:讓「原本應該消失的區域變數」延續生命,並綁定在函式值上,隨著函式一起被保存下來。
結語:閉包的價值與實用性
閉包讓我們能夠:
- 建立有記憶能力的函式。
- 實作私有變數。
- 延長變數的生命週期。
- 用更清晰、有彈性的方式封裝邏輯。
雖然一開始會覺得抽象,但透過實際練習與範例觀察,你會發現閉包是 JavaScript 非常核心且實用的一個概念。
如果你還是覺得困惑,可以試著自己動手寫一個小程式,把變數包進函式裡面,再慢慢觀察它的行為。相信你會越來越熟悉這個強大的語言特性!