ข้ามไปเนื้อหาหลัก

Category: guide

JavaScript Closures และ Scope — เข้าใจจริง ไม่ใช่แค่ท่อง

Scope chain, lexical environment, closures ทำงานยังไง, และ use cases จริงที่เจอบ่อย

· อ่านประมาณ 4 นาที

สารบัญ

Scope คืออะไร

Scope กำหนดว่า variable ไหนเข้าถึงได้จาก context ไหน JavaScript มี 3 ระดับ:

// Global scope — เข้าถึงได้ทุกที่
const APP_NAME = 'My App';

function outer() {
  // Function scope — เข้าถึงได้เฉพาะใน outer()
  const x = 10;

  if (true) {
    // Block scope — เข้าถึงได้เฉพาะใน if block
    const y = 20;
    let z = 30;
    console.log(x); // ✓ เห็น parent scope
  }

  console.log(y); // ✗ ReferenceError
}

var ไม่มี block scope — นี่คือเหตุผลหลักที่ต้องใช้ let/const:

if (true) {
  var leak = 'visible outside'; // hoisted ขึ้น function scope
  let safe = 'block only';
}
console.log(leak); // ✓ "visible outside"
console.log(safe); // ✗ ReferenceError

Scope Chain

เมื่อ JS หา variable จะขึ้น scope ไปเรื่อยๆ จนถึง global:

const a = 1;

function outer() {
  const b = 2;

  function inner() {
    const c = 3;
    console.log(a, b, c); // ✓ เห็นทุกตัวผ่าน scope chain
  }

  inner();
  console.log(c); // ✗ ReferenceError — ลงไปไม่ได้
}

Scope chain ถูกกำหนด ณ ตอนที่เขียนโค้ด (lexical) ไม่ใช่ตอน runtime


Lexical Scope vs Dynamic Scope

JavaScript ใช้ lexical scope — ตำแหน่งใน source code กำหนด scope:

const name = 'global';

function greet() {
  console.log(name); // อ่านจาก scope ตอนเขียน — "global"
}

function test() {
  const name = 'local';
  greet(); // เรียก greet แต่ greet ยังเห็น "global" ไม่ใช่ "local"
}

test(); // "global"

Closure คืออะไร

Closure คือ function ที่ “จำ” lexical scope ที่มันถูกสร้างขึ้น แม้ scope นั้นจะ execute เสร็จแล้ว:

function makeCounter() {
  let count = 0; // variable นี้ "ติด" อยู่กับ function ข้างใน

  return function () {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// makeCounter() เสร็จแล้ว แต่ count ยังอยู่เพราะ closure

Use Case 1: Data Privacy

function createBankAccount(initialBalance) {
  let balance = initialBalance; // private — ไม่มีทางเข้าถึงจากภายนอกโดยตรง

  return {
    deposit(amount) {
      balance += amount;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error('Insufficient funds');
      balance -= amount;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.balance);     // undefined — ซ่อนอยู่ใน closure

Use Case 2: Function Factory

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

const double = multiplier(2);
const triple = multiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

แต่ละ call ของ multiplier สร้าง closure ใหม่ที่มี factor ของตัวเอง


Use Case 3: Memoization

function memoize(fn) {
  const cache = new Map(); // closure เก็บ cache

  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  console.log('Computing...');
  return n * n;
});

expensiveCalc(10); // "Computing..." → 100
expensiveCalc(10); // (ไม่ log) → 100 จาก cache

Use Case 4: Event Handlers

function setupButtons(items) {
  items.forEach((item, index) => {
    const btn = document.createElement('button');
    btn.textContent = item;

    // closure capture ค่า item และ index ณ iteration นี้
    btn.addEventListener('click', () => {
      console.log(`Clicked: ${item} at index ${index}`);
    });

    document.body.appendChild(btn);
  });
}

setupButtons(['Apple', 'Banana', 'Cherry']);

ปัญหาคลาสสิกกับ var (ก่อน let):

// ❌ var — closure ทุกตัวชี้ไปที่ตัวแปรเดียวกัน
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 3, 3, 3 — ไม่ใช่ 0, 1, 2

// ✓ let — แต่ละ iteration มี scope ของตัวเอง
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 0, 1, 2

Use Case 5: Partial Application

function partial(fn, ...preArgs) {
  return function (...laterArgs) {
    return fn(...preArgs, ...laterArgs);
  };
}

function add(a, b, c) { return a + b + c; }

const add10 = partial(add, 10);
console.log(add10(5, 3)); // 18

// ใช้กับ API calls
const getUser = (baseUrl, userId) => fetch(`${baseUrl}/users/${userId}`);
const getLocalUser = partial(getUser, 'http://localhost:3000');
getLocalUser(42); // fetch http://localhost:3000/users/42

Hoisting

var declarations ถูก hoist (ยก) ขึ้นบน function scope — แต่ค่ายังเป็น undefined:

console.log(x); // undefined — ไม่ใช่ ReferenceError
var x = 5;
console.log(x); // 5

let/const ก็ถูก hoist แต่อยู่ใน “Temporal Dead Zone” จนถึงบรรทัดที่ประกาศ:

console.log(y); // ✗ ReferenceError: Cannot access 'y' before initialization
let y = 5;

Functions declarations ถูก hoist ทั้งหมด — เรียกก่อนประกาศได้:

greet(); // "Hello" — ใช้งานได้

function greet() {
  console.log('Hello');
}

แต่ function expressions ไม่ได้:

greet(); // ✗ TypeError: greet is not a function
var greet = function () { console.log('Hello'); };

Memory Leak จาก Closure

Closure ป้องกัน garbage collection ของตัวแปรที่ถูก reference — ถ้าไม่ระวัง:

function setup() {
  const largeArray = new Array(1_000_000).fill(0); // 8MB

  // ❌ closure ที่ไม่จำเป็นทำให้ largeArray อยู่ใน memory ตลอด
  document.addEventListener('click', () => {
    console.log(largeArray.length);
  });
}

// ✓ ถ้าไม่ต้องการ largeArray แล้ว ให้ remove event listener
function teardown(handler) {
  document.removeEventListener('click', handler);
}

สรุป Pattern

Conceptทำอะไรได้
Closureเก็บ state โดยไม่ต้องใช้ global variable
Function Factoryสร้าง functions ที่ configured มาแล้ว
Memoizationcache ผลลัพธ์ที่คำนวณแล้ว
Module Patternสร้าง private state แบบ OOP-lite
Partial Applicationlock บาง arguments ของ function ไว้