← JavaScript Learning Path

JAVASCRIPT V2 · ADVANCED · 126

Closure / Lexical Scope:Function 為什麼能帶著外層 Binding 繼續活著?

125 讓 function 變成可傳遞的 value。下一個自然問題是:function 被傳到原本 scope 外面後,為什麼仍能讀取建立當下的外層資料?答案不是「JavaScript 幫你複製值」,而是 lexical environment 與 closure。

Learning outcomes

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

15. Further reading

Knowledge check · Mastery

  1. 說明 closure 保存 binding access,而不是單純複製值。
  2. 預測 var 與 let loop callback 差異。
  3. 完成 repository 與 memoizeOne。
  4. 完成頁面下方 Execution Trace mastery。