DevToolBoxฟรี
บล็อก

JavaScript Closures อธิบาย: Scope, Memory และ Patterns จริง

13 นาทีโดย DevToolBox

Closures are one of the most fundamental and powerful features in JavaScript. A closure is created when a function is defined inside another function and retains access to the outer function's variables even after the outer function has returned. Understanding closures deeply will change how you architect JavaScript code — from module patterns to React hooks.

What is a Closure?

Every time a function is created in JavaScript, a closure is formed. The function "closes over" its surrounding lexical environment — the variables in scope at the time of the function's definition.

// What is a closure?
// A closure is a function that has access to variables
// from its outer (enclosing) lexical scope, even after
// that scope has returned.

function makeCounter() {
  let count = 0;    // private variable in the outer scope

  return function() {
    count++;        // inner function accesses outer variable
    return count;
  };
}

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

// count is NOT accessible from outside:
// console.log(count); // ReferenceError!

// Each call to makeCounter() creates a NEW closure
const counter2 = makeCounter();
console.log(counter2()); // 1 (independent state)
console.log(counter());  // 4 (original counter continues)

Scope Chain and Variable Capture

Closures capture variables by reference, not by value. This is a source of both power and common bugs.

// Closure captures the ENTIRE scope chain, not just one level
const globalVar = 'global';

function outer() {
  const outerVar = 'outer';

  function middle() {
    const middleVar = 'middle';

    function inner() {
      // Has access to ALL enclosing scopes
      console.log(globalVar);  // 'global'
      console.log(outerVar);   // 'outer'
      console.log(middleVar);  // 'middle'
    }

    inner();
  }

  middle();
}

outer();

// Variables are captured by REFERENCE, not by value
function makeAdder(x) {
  // x is captured by reference
  return function(y) {
    return x + y;   // x is still accessible
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(3));   // 8
console.log(add10(3));  // 13

The Classic Loop Bug (and Fixes)

The most infamous closure bug: using var in a loop with asynchronous callbacks. Understanding this reveals exactly how closures capture variables.

// CLASSIC BUG: var in loops (before let/const existed)

// Wrong: all callbacks share the same i variable
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);   // prints 3, 3, 3 (not 0, 1, 2!)
  }, 100);
}
// Reason: var is function-scoped, one shared i that becomes 3

// Fix 1: use let (creates a new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);   // prints 0, 1, 2 ✓
  }, 100);
}

// Fix 2: IIFE (Immediately Invoked Function Expression) — old school
for (var i = 0; i < 3; i++) {
  (function(capturedI) {
    setTimeout(function() {
      console.log(capturedI);  // prints 0, 1, 2 ✓
    }, 100);
  })(i);
}

// Fix 3: factory function
function makeTimeout(index) {
  return function() {
    console.log(index);  // captured by value in this scope
  };
}

for (var i = 0; i < 3; i++) {
  setTimeout(makeTimeout(i), 100);   // 0, 1, 2 ✓
}

Practical Closure Patterns

Closures enable several powerful programming patterns: the module pattern, memoization, partial application, and more.

// CLOSURE PATTERNS

// 1. Module Pattern — private state with public API
const bankAccount = (function() {
  let balance = 0;    // private

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

bankAccount.deposit(100);
bankAccount.withdraw(30);
console.log(bankAccount.getBalance());  // 70
// console.log(balance); // ReferenceError

// 2. Memoization — cache expensive computations
function memoize(fn) {
  const cache = new Map();   // cache lives in the closure

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

const slowFibonacci = (n) => n <= 1 ? n : slowFibonacci(n - 1) + slowFibonacci(n - 2);
const fastFibonacci = memoize(slowFibonacci);

// 3. Partial application
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

const multiply = (a, b) => a * b;
const double = partial(multiply, 2);
const triple = partial(multiply, 3);

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

// 4. Once — run a function only one time
function once(fn) {
  let called = false;
  let result;

  return function(...args) {
    if (!called) {
      called = true;
      result = fn(...args);
    }
    return result;
  };
}

const initialize = once(() => {
  console.log('Initialized!');
  return { status: 'ready' };
});

initialize();  // Initialized!
initialize();  // (nothing logged)
initialize();  // (nothing logged)

Memory Management with Closures

Closures keep references to their enclosing scope alive. This is necessary for them to work, but can cause memory leaks if not managed carefully.

// MEMORY CONSIDERATIONS

// Closures keep references alive — potential memory leaks

// BAD: Large object kept alive by closure
function processData(hugeArray) {
  // processedResult only needs a small piece of hugeArray
  const processedResult = hugeArray[0].value;

  // But this closure keeps hugeArray alive in memory
  return function getResult() {
    return processedResult;  // only uses processedResult
    // However, the closure scope contains hugeArray!
  };
}

// BETTER: Narrow the scope so hugeArray can be GC'd
function processDataFixed(hugeArray) {
  const processedResult = hugeArray[0].value;
  hugeArray = null;  // allow GC (if no other references)

  return function getResult() {
    return processedResult;
  };
}

// REAL-WORLD: Event listener leaks
class Component {
  constructor() {
    this.data = new Array(1000000).fill('x');  // large data
    this.handler = (event) => {
      console.log(this.data.length);  // closure over 'this'
    };
    document.addEventListener('click', this.handler);
  }

  // MUST clean up to prevent memory leak
  destroy() {
    document.removeEventListener('click', this.handler);
  }
}

Closures in React: The Stale Closure Problem

React hooks rely heavily on closures, which can lead to the "stale closure" problem where event handlers or effects capture outdated state.

// Closures in React — the stale closure problem

import { useState, useEffect, useCallback, useRef } from 'react';

// PROBLEM: stale closure
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      // count is captured at the time the effect runs (= 0)
      console.log(count);  // always logs 0! (stale closure)
    }, 1000);

    return () => clearInterval(interval);
  }, []);  // empty deps = runs once = captures count=0

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// FIX 1: Add count to dependency array
useEffect(() => {
  const interval = setInterval(() => {
    console.log(count);  // fresh value (but re-creates interval)
  }, 1000);
  return () => clearInterval(interval);
}, [count]);

// FIX 2: Use a ref (mutable, doesn't trigger re-render)
function CounterWithRef() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count;  // always up to date

  useEffect(() => {
    const interval = setInterval(() => {
      console.log(countRef.current);  // always fresh!
    }, 1000);
    return () => clearInterval(interval);
  }, []);  // no deps needed

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Closure vs Class: When to Use Which

AspectClosureClass
Private stateLocal variables in factory functionPrivate class fields (#field)
Memory (single instance)More efficient (no prototype)Less efficient (prototype overhead)
Memory (many instances)Less efficient (each has own methods)More efficient (shared prototype methods)
InheritanceVia composition / Object.assignVia extends keyword
this bindingNo this issues (functional style)Requires careful this management
ReadabilityFunctional, compactOOP, familiar to Java/C# devs
PerformanceSlightly faster for closuresSlightly faster method calls

Frequently Asked Questions

Are closures the same as IIFE (Immediately Invoked Function Expressions)?

No, but they are related. An IIFE is a function that is defined and immediately called. It creates a new scope, and any functions returned from it will form closures over that scope. IIFEs were the primary way to create private scope before ES modules and block-scoped let/const. Today, ES modules provide better encapsulation.

Do arrow functions create closures?

Yes. Arrow functions are not special in terms of closures — they capture variables from their surrounding lexical scope just like regular functions. The key difference is that arrow functions do not have their own this binding, so they capture this from the surrounding scope too.

What is the difference between a closure and a class?

Both can maintain private state, but differ in approach. Closures use function scope to encapsulate state — private variables are simply local variables in the factory function. Classes use class fields (with # prefix for private fields in modern JS). Closures are more memory-efficient when creating one instance; classes (using prototypes) are more memory-efficient for multiple instances since methods are shared on the prototype.

How do closures relate to garbage collection?

A closure keeps its enclosing scope alive as long as the closure itself is reachable. If you create a closure that references a large object, that object will not be garbage collected as long as the closure exists. This is the primary source of closure-related memory leaks. Always remove event listeners and clear timers when they are no longer needed.

Why do React hooks depend on closures?

React hooks like useState and useEffect return functions that close over the component's state and props at the time of rendering. Each render creates a new closure with a snapshot of the current state. This is why useEffect dependencies are important — including stale values in the dependency array ensures React re-runs the effect with fresh closures when those values change.

Related Tools

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

JSJavaScript Minifier{ }JSON FormatterTSTypeScript Playground

บทความที่เกี่ยวข้อง

TypeScript 5 ฟีเจอร์ใหม่: Decorators, Const Type Parameters และ Satisfies

คู่มือครบครัน TypeScript 5: decorators, const type parameters, satisfies operator

TypeScript Type Guards: คู่มือการตรวจสอบประเภทขณะรันไทม์

เชี่ยวชาญ TypeScript type guards: typeof, instanceof, in และ guard แบบกำหนดเอง

การเพิ่มประสิทธิภาพเว็บ: คู่มือ Core Web Vitals 2026

คู่มือฉบับสมบูรณ์สำหรับการเพิ่มประสิทธิภาพเว็บและ Core Web Vitals เรียนรู้วิธีปรับปรุง LCP, INP และ CLS ด้วยเทคนิคจริงสำหรับรูปภาพ JavaScript CSS และแคช