DevToolBox免费
博客

JavaScript 闭包详解:作用域、内存与实战模式

13 分钟作者 DevToolBox

闭包是 JavaScript 中最基础、最强大的特性之一。当函数在另一个函数内部定义,并且即使在外部函数返回后仍然保留对外部函数变量的访问权限时,就会创建闭包。深入理解闭包将改变你构建 JavaScript 代码的方式——从模块模式到 React hooks。

什么是闭包?

每次在 JavaScript 中创建函数时,都会形成一个闭包。函数"关闭"了其周围的词法环境——函数定义时作用域内的变量。

// 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)

作用域链和变量捕获

闭包通过引用捕获变量,而非按值捕获。这既是力量的来源,也是常见错误的根源。

// 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

经典循环错误(及修复方法)

最臭名昭著的闭包错误:在循环中使用 var 配合异步回调。理解这个问题能揭示闭包如何捕获变量。

// 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 ✓
}

实用的闭包模式

闭包支持几种强大的编程模式:模块模式、记忆化、偏应用等。

// 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 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);
  }
}

React 中的闭包:陈旧闭包问题

React hooks 严重依赖闭包,这可能导致"陈旧闭包"问题,即事件处理器或副作用捕获了过时的状态。

// 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>;
}

闭包 vs 类:何时使用哪个

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

常见问题

闭包和 IIFE(立即调用函数表达式)一样吗?

不一样,但有关联。IIFE 是定义后立即调用的函数。它创建一个新作用域,从它返回的任何函数都会对该作用域形成闭包。在 ES 模块和块作用域 let/const 之前,IIFE 是创建私有作用域的主要方式。今天,ES 模块提供了更好的封装。

箭头函数会创建闭包吗?

是的。就闭包而言,箭头函数没有什么特别之处——它们像普通函数一样从周围的词法作用域捕获变量。关键区别在于箭头函数没有自己的 this 绑定,所以它们也从周围作用域捕获 this。

闭包和类有什么区别?

两者都可以维护私有状态,但方法不同。闭包使用函数作用域封装状态——私有变量只是工厂函数中的局部变量。类使用类字段(现代 JS 中私有字段用 # 前缀)。创建一个实例时,闭包更节省内存;对于多个实例,类(使用原型)更节省内存,因为方法在原型上共享。

闭包与垃圾回收有什么关系?

只要闭包本身可以被访问,它就会保持其封闭作用域。如果你创建了一个引用大型对象的闭包,只要闭包存在,该对象就不会被垃圾回收。这是闭包相关内存泄漏的主要来源。当事件监听器和定时器不再需要时,始终移除它们。

为什么 React hooks 依赖闭包?

useState 和 useEffect 等 React hooks 返回的函数会关闭组件在渲染时的状态和 props。每次渲染都会创建一个包含当前状态快照的新闭包。这就是为什么 useEffect 依赖项很重要——在依赖数组中包含陈旧值确保 React 在这些值改变时使用新鲜闭包重新运行副作用。

相关工具

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

JSJavaScript Minifier{ }JSON FormatterTSTypeScript Playground

相关文章

TypeScript 5 新特性:装饰器、const 类型参数与 satisfies 运算符

TypeScript 5 新特性完全指南:装饰器、const 类型参数、satisfies 运算符及性能改进。

TypeScript 类型守卫:运行时类型检查完全指南

掌握 TypeScript 类型守卫:typeof、instanceof、in、自定义类型守卫和可辨识联合。

Web 性能优化:2026 Core Web Vitals 指南

全面的 Web 性能优化和 Core Web Vitals 指南。学习如何通过图片、JavaScript、CSS 和缓存的实用技术改善 LCP、INP 和 CLS。