JAVASCRIPT V2 · ADVANCED · 126
Closure / Lexical Scope:Function 為什麼能帶著外層 Binding 繼續活著?
125 讓 function 變成可傳遞的 value。下一個自然問題是:function 被傳到原本 scope 外面後,為什麼仍能讀取建立當下的外層資料?答案不是「JavaScript 幫你複製值」,而是 lexical environment 與 closure。
Learning outcomes
- 區分 lexical scope 與 call location。
- 理解 closure 保存的是對 lexical environment 中 binding 的存取能力。
- 能追蹤多個 factory calls 為何形成獨立 state。
- 能用 closure 建立 encapsulated state 與 configurable function。
- 辨識 var loop、共享 mutable state 與 stale assumptions。
1. Why now:Callback 被延後呼叫後,資料從哪裡來?
function makeGreeter(
greeting
) {
return name =>
greeting + " " + name;
}
const hello =
makeGreeter("Hello");
hello("Amy");
makeGreeter 已經 return 完成,但 hello 仍能讀 greeting。這就是 closure 最直觀的用途。
2. Mental model:Scope 看 source structure,不看誰呼叫
const tax = 0.05;
function total(price) {
return price * (1 + tax);
}
function run(fn) {
const tax = 0.99;
return fn(100);
}
run(total);
total 讀的是它定義位置可見的 tax,不是 run 裡剛好存在的另一個 tax。這就是 lexical scope。
3. Worked Example A:Counter closure
function createCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter =
createCounter();
counter(); // 1
counter(); // 2
外層 activation 已經完成,但 inner function 仍引用 count,因此相關 environment 仍可被存取。
4. Execution trace:不是把 count 複製進 function
createCounter()
→ create binding count = 0
→ create increment function
→ increment closes over environment
→ return function
counter()
→ find count in closed-over environment
→ count = 1
counter()
→ same binding
→ count = 2
如果只是「複製初始值 0」,第二次呼叫就不可能看到更新後的 1。
5. Worked Example B:每次 factory call 有自己的 environment
const a = createCounter();
const b = createCounter();
a(); // 1
a(); // 2
b(); // 1
environment A:
count = 2
environment B:
count = 1
a 與 b 共用同一段 function code 形狀,但各自 closure 到不同 invocation 建立的 state。
6. Worked Example C:Private-like state
function createStore() {
let items = [];
return {
list() {
return [...items];
},
add(item) {
items = [...items, item];
}
};
}
const store = createStore();
外部沒有直接拿到 items binding,只能經公開 methods 操作。這是 encapsulation,不代表資料具備安全邊界;執行同一 JavaScript context 的程式仍可能透過你暴露的 API 改狀態。
7. Closure 與 callback 會一起出現
function wireButton(
button,
studentId
) {
button.addEventListener(
"click",
() => {
selectStudent(
studentId
);
}
);
}
event callback 延後執行,但仍能取得 wireButton invocation 的 studentId。
8. Common mistakes
A. 把 closure 說成「function 記住 value」
更精確是可存取 lexical binding;binding 的值之後可以改。
B. 以為兩次 factory call 共用 state
通常每次 invocation 建立自己的 environment。
C. 無意間共享 mutable object
const shared = [];
function makeAdder() {
return item => {
shared.push(item);
};
}
所有 returned functions 都碰同一個外部 shared。
D. var loop callback
多個 callbacks 可能都捕捉同一個 function-scoped binding。
9. Debug evidence:確認你捕捉的是哪一個 binding
for (var i = 0; i < 3; i++) {
setTimeout(
() => console.log(i),
0
);
}
// 3 3 3
證據不是「timer 有問題」,而是三個 callback 都讀同一個 i binding;callback 真正執行時 loop 已結束。
10. let 為什麼改變結果
for (let i = 0; i < 3; i++) {
setTimeout(
() => console.log(i),
0
);
}
// 0 1 2
for-loop 的 let 會為每一輪建立對應 binding,callbacks 因此連到不同 environment。
11. Guided exercise:可設定的 validator
function between(
min,
max
) {
// return validator
}
const isScore =
between(0, 100);
validator 必須在 between return 後仍能讀 min/max,並回 Boolean。
12. Independent exercise:Student repository
用 closure 寫 createStudentRepository(),公開 list/add/remove/findById,但不直接暴露內部 array。list() 不得把可直接 push 的內部 array reference 原封不動交出去。
const repo =
createStudentRepository();
repo.add({
id: 1,
name: "Amy"
});
repo.findById(1);13. Challenge:Memoization 的 state 在哪裡?
function memoizeOne(fn) {
let hasValue = false;
let lastArg;
let lastResult;
return arg => {
// TODO
};
}
只要輸入與前一次相同就回 cached result。說明哪些 bindings 被 closure 保存、何時更新。
Project checkpoint:Grade Tracker Store
function createStudentStore() {
let students = [];
function emit() {
// render / subscriber hook
}
return {
list() {
return [...students];
},
add(student) {
students = [
...students,
student
];
emit();
}
};
}
Store 把 state ownership 收到明確 boundary;下一步可與 131 的 pure update / reducer 模型比較。
14. Vocabulary / Summary
- lexical scope:由 source nesting 決定的名稱可見範圍。
- lexical environment:binding 與 outer environment 關係。
- closure:function 與其建立時 lexical environment 的連結。
- encapsulation:限制 state 的直接操作面。
- factory function:建立並回傳 configured object/function 的 function。
15. Further reading
Knowledge check · Mastery
- 說明 closure 保存 binding access,而不是單純複製值。
- 預測 var 與 let loop callback 差異。
- 完成 repository 與 memoizeOne。
- 完成頁面下方 Execution Trace mastery。