JavaScript 初學者教學:什麼是閉包(Closure)?

Published July 14, 2025 by 徐培鈞
JavaScript

在學習 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); // ❌ 錯誤:找不到 name

name 是貼在「內部工作區」的資料,外面的訪客(也就是整個程式的其他部分)是沒資格看到的。

這不是因為資料不見了,而是你沒有權限看見它。

🎯 為什麼要這樣設計?

這種「有權限才能看資料」的設計,其實是為了讓程式更有秩序。

想像一下,如果公司把所有內部文件都貼在大廳,任何人都能看、能改,會發生什麼事?

  • 訪客可能誤改資料
  • 不同部門的人可能互相干擾
  • 系統變得很難維護,容易出錯

所以,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);

這一行的意思是:

  1. 我們呼叫 wrapValue(1),傳進數字 1 作為參數;
  2. 進入 wrapValue 裡,JavaScript 幫我們建立了一個新的資料空間,這裡面有個變數 local = 1
  3. 接著,程式定義了一個函式 getValue,裡面寫著「當我被呼叫時,請回傳 local 的值」;
  4. 然後,把這個 getValue 函式「回傳到外面」,交給外部程式保存;
  5. 外面的程式把這個函式值存進變數 wrap1 裡。

此時的 wrap1 是什麼?

是一個記得 local = 1 的小函式

這個函式不但還活著,還擁有內部那份資料的「存取權限」!

第二步:呼叫 wrapValue(2)

let wrap2 = wrapValue(2);

這一行是跟剛剛同樣的流程,只是這次我們傳進 2

  1. 再次呼叫 wrapValue(2),建立另一份資料空間,這次的 local = 2
  2. 同樣建立一個新的 getValue 函式,會回傳這個新的 local
  3. 把這個函式傳回外面,存進 wrap2

注意!這裡雖然程式碼長得一模一樣,但:

  • 每一次呼叫 wrapValue(),都會產生全新的 local 和全新的 getValue 函式
  • wrap1wrap2 雖然來自同一個模版,但他們各自記得的是不同的資料,互不干擾!

第三步:執行這些函式

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 = 1local = 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 裡,只要你宣告了一個函式,它的每個參數(像是 namefactornumber 這些)就會自動變成這個函式的「內部變數」——也就是區域變數

你不需要額外用 letconst 去宣告它,它就已經存在了,而且:

它的使用範圍只限在這個函式裡面,外面是完全看不到、也用不到的。

👀 來看個例子:

function sayHello(name) {
  console.log('哈囉,' + name);
}

sayHello('小明');
console.log(name); // ❌ 錯誤,name is not defined

這裡的 namesayHello 的一個參數,但你會發現:

  • 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);

會發生以下幾件事:

  1. JavaScript 進入 multiplier 函式的內部。
  2. 這時 factor = 2,這是一個區域變數,只在這次的 multiplier 呼叫中有效。
  3. 接著,函式回傳一個 匿名函式
   function (number) {
     return number * factor;
   }

(這只是簡化語法的說法,實際是箭頭函式 number => number * factor

  1. 雖然 multiplier() 呼叫完成了,但這個被回傳出去的匿名函式,帶著對當時 factor = 2 的記憶被交給了外面的 twice

你可以這樣想:我們叫 multiplier 這位部門主管幫我們生出一個員工 twice,這位員工身上配有一張專屬工作證,上面寫著:「我可以查閱 factor = 2」。即使他之後不再待在原本部門內,這份權限依然有效。

🧩 第二步:呼叫 twice(5)

接著執行這行:

console.log(twice(5)); // → 10

這其實就是在執行剛剛回傳的匿名函式。流程如下:

  1. 我們將數字 5 傳進 twice(),這時匿名函式裡的參數 number = 5
  2. 函式裡執行的內容是:
   return number * factor;

number 是現在傳進來的 5,
factor 則是早在第一次呼叫 multiplier(2) 時就記下來的 2(由閉包機制保存下來)。

  1. 所以最終計算是:5 * 2,得到結果 10

換句話說:即使 factor 這個變數早就離開 multiplier 的範圍,因為我們透過閉包把它「打包帶出來」,現在仍然可以使用它。

wrapValue 和 multiplier 的比較

我們剛剛看了兩個範例,它們其實都在做同一件事:

建立一個「有權限存取某個變數」的函式,然後把它傳到外面繼續使用。

但這兩個範例的寫法風格不太一樣。

wrapValue 的寫法比較「明確」

function wrapValue(n) {
  let local = n;

  let getValue = function () {
    return local;
  };

  return getValue;
}

這裡我們做了三件事:

  1. let 宣告了一個變數 local,這是這次呼叫專屬的「區域資料表」;
  2. 寫了一個匿名函式,這段程式會讀取 local,並把它指定給變數 getValue
  3. 把這個 getValue 函數值回傳出去,讓外部的程式可以透過它來查資料。

也就是說,我們不是把資料直接交出去,而是「派出一個知道怎麼查資料的代理人」。

這個代理人(函數值)來自限制區內部,擁有合法工作證,所以能夠存取 local

multiplier 的寫法更「簡潔」

function multiplier(factor) {
  return number => number * factor;
}

這個版本看起來簡短很多,實際上做的事情是一樣的。

你可以這樣理解:

  1. 傳進來的 factor,其實也是一份「內部資料表」;
  2. 我們沒有額外宣告變數,而是直接寫一個箭頭函式(匿名函式的縮寫),讓它記住 factor
  3. 然後把這個函式回傳出去,用來乘以其他數字。

這邊就沒用變數包裝,而是一步到位就把會用到內部變數的函式交出去

兩者的差別比喻一下

你可以把 wrapValue 想成:

先建資料 → 指派代理人 → 把代理人派出去

multiplier 則像是:

當場建好代理人,順手就送出去了

兩者結果是一樣的,都是「產生一個擁有特定內部資料權限的函式值」,只是寫法的風格不同。

最重要的觀念

無論你是像 wrapValue 那樣分工明確,還是像 multiplier 那樣簡化寫法,它們都在證明一件事:

函式在被回傳出去時,會「帶著」它出生當下的變數環境(也就是閉包)。

這就是閉包的核心能力:讓「原本應該消失的區域變數」延續生命,並綁定在函式值上,隨著函式一起被保存下來。

結語:閉包的價值與實用性

閉包讓我們能夠:

  • 建立有記憶能力的函式。
  • 實作私有變數。
  • 延長變數的生命週期。
  • 用更清晰、有彈性的方式封裝邏輯。

雖然一開始會覺得抽象,但透過實際練習與範例觀察,你會發現閉包是 JavaScript 非常核心且實用的一個概念。

如果你還是覺得困惑,可以試著自己動手寫一個小程式,把變數包進函式裡面,再慢慢觀察它的行為。相信你會越來越熟悉這個強大的語言特性!