iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
JavaScript

JavaScript為什麼筆記本系列 第 26 篇

Day 26|外面的 function 都跑完了,裡面的 count 為什麼還記得?

  • 分享至 

  • xImage
  •  

Day 25 最後留了一個計數器。先不急著背 Closure 的定義,把程式接著跑下去。

function createCounter() {
  let count = 0

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

const counter = createCounter()

console.log(counter())
console.log(counter())

結果是:

1
2

等一下。createCounter() 在建立 counter 時,不就已經執行完了嗎?

count 又不是全域變數。為什麼第一次呼叫記得從 0 開始,第二次還知道上次已經加到 1?

今天只追這個問題:外層函式結束了,內層函式為什麼還能使用它的變數?

先看清楚,這裡有兩個不同的呼叫

const counter = createCounter()

這一行呼叫的是 createCounter。它建立 count,再回傳裡面的 increment 函式。

回傳的是「函式本身」,不是先執行 increment()、再把結果交出來。函式可以當成值交給其他變數,這正好接回 Day 22。

所以流程是:

createCounter()
    ↓
建立這次呼叫的 count = 0
    ↓
建立 increment 函式
    ↓
把 increment 交給外面的 counter

之後寫:

counter()

才是在執行剛才拿到的內層函式。

它沒有重新呼叫一次 createCounter(),所以也沒有再執行一次 let count = 0。

先把這兩次呼叫分開,計數器就沒那麼神祕了。

函式帶走的不只是程式碼

Day 24 的 Scope Chain 說的是:函式裡找不到的名稱,會沿著它建立時的外層環境繼續找。

increment 裡沒有自己的 count,但它是在這次 createCounter() 的環境裡建立的。因此,它能存取那個 count。

重點是:把函式交到外面,並不會切斷這個連結。

counter
    ↓ 指向
increment 函式
    ↓ 仍能存取
建立它時的外層環境
    ↓
count

這個「函式,加上它能存取的詞法環境」,就是 Closure,中文常叫閉包。

比較有用的問題是:

這個函式建立在哪裡?它現在用到的變數,屬於哪一次呼叫?

執行結束,不等於變數一定立刻消失

createCounter() 確實已經執行完,不是偷偷在背景繼續跑。

但回傳的函式還能到達它需要的環境;不能只因為外層函式返回,就把之後仍然需要的資料當成無用資料丟掉。這和垃圾回收所關心的「還能不能被存取」有關。

這也不代表整個函式、整段堆疊都必須原封不動留著。引擎可以最佳化;理解行為時,只要抓住仍可存取的變數就好。

現在再看兩次呼叫:

第一次 counter()
同一個 count:0 → 1

第二次 counter()
同一個 count:1 → 2

所以第二次得到 2,不是 JavaScript 記得上一個回傳值,而是內層函式又修改了同一個變數。

Closure 不是把當時的數字拍照保存

如果聽到「記住外面的變數」,很容易想成:函式建立時,把當時的值複製一份。

換一個最小例子試試:

function createReader() {
  let message = 'before'

  const read = () => message

  message = 'after'
  return read
}

const read = createReader()
console.log(read())

結果是 after,不是 before。

read 存取的是那個 message 變數,不是建立函式時拍下來的字串照片。對可重新賦值的變數來說,之後讀到的是該變數當下的值。

這也能解釋前面的 count 為什麼可以一直增加,而不是每次都拿著一份固定的 0。

那建立兩個計數器,會不會混在一起?

沿用前面的 createCounter,再看:

const counterA = createCounter()
const counterB = createCounter()

console.log(counterA())
console.log(counterA())
console.log(counterB())
console.log(counterA())

結果是:

1
2
1
3

函式程式碼看起來一樣,不代表使用同一份變數。這裡呼叫了兩次 createCounter(),各自建立新的環境。

第一次 createCounter()       第二次 createCounter()
          ↓                           ↓
      count A = 0                 count B = 0
          ↑                           ↑
       counterA                    counterB

所以 A 加到 2,不會順便把 B 加到 2。

能透過函式使用,不代表外面直接看得到

在前面例子的外層另外執行:

console.log(count)

會得到 ReferenceError,因為那裡沒有可用的 count 綁定。

Closure 沒有把區域變數變成全域變數。外面拿到的是能使用它的函式,不是直接取得那個名稱。

也不是一定要寫 return function 才會產生 Closure;事件處理函式或其他 callback 也可能使用外層環境。這次用回傳函式,只是比較容易看出外層呼叫已經結束。

把 count 放回呼叫裡面,結果就不同了

function counterThatResets() {
  let count = 0
  count += 1
  return count
}

console.log(counterThatResets())
console.log(counterThatResets())

結果變成:

1
1

因為每次呼叫都會重新建立自己的 count,並執行 let count = 0。這次回傳的是數字,不是保留外層變數存取能力的計數函式。

今天真正要分清楚的不是「裡面有沒有 function」,而是:每次呼叫時,用到的是新建立的變數,還是先前那一份?

先別往下看,猜一次

仍然沿用 createCounter:

const first = createCounter()
const second = first
const third = createCounter()

console.log(first())
console.log(second())
console.log(third())

先猜三行結果,再說說:second = first 有沒有呼叫 createCounter()?

1
2
1

first 和 second 指向同一個函式,因此操作同一份 count。只有 third 是另外呼叫 createCounter() 建立的新計數器。

這不是變數名稱不同,就自動得到一份新資料。

如果今天只記得一件事

Closure 讓函式持續存取建立它時的外層變數;外層函式返回,不代表這份存取關係就消失。

讀程式時,先找出「哪個函式建立在哪裡」,再問「這個變數屬於哪一次呼叫」。比只背「函式包住函式」更能解釋結果。


上一篇
Day 25|Hoisting 真的是把程式碼搬到最上面嗎?為什麼 function 可以先用,const 卻不行?
下一篇
Day 27|同一個 function,為什麼換個呼叫方式,this 就變了?
系列文
JavaScript為什麼筆記本 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言