Home
avatar

TheBlockSky

深入理解 JavaScript 闭包

什么是闭包?

闭包(Closure)是 JavaScript 中最核心也最常被问到的概念之一。简单来说,闭包是指一个函数能够记住并访问其词法作用域(Lexical Scope)中的变量,即使这个函数在其词法作用域之外执行。

function outer() {
  const message = 'Hello from closure!';
  
  function inner() {
    console.log(message);
  }
  
  return inner;
}

const fn = outer();
fn(); // 输出: Hello from closure!

上面的 inner 函数就是一个闭包——它”捕获”了 message 变量,即使在 outer 执行完毕后仍然可以访问它。


闭包的工作原理

词法作用域

JavaScript 采用词法作用域(也称静态作用域),这意味着函数的作用域在定义时就已经确定,而不是在调用时。

const globalVar = 'global';

function outer() {
  const outerVar = 'outer';
  
  function inner() {
    const innerVar = 'inner';
    console.log(globalVar); // 可访问
    console.log(outerVar);  // 可访问
    console.log(innerVar);  // 可访问
  }
  
  inner();
}

outer();

作用域链

每个函数在定义时都会创建一个作用域链(Scope Chain),它包含了当前函数的所有外层作用域的引用。当函数执行并访问变量时,JavaScript 引擎会沿着这条链逐层查找。

执行上下文与词法环境

当函数执行时,会创建一个执行上下文(Execution Context),其中包含:

  • 变量环境(Variable Environment):存储 letconst 声明的变量
  • 词法环境(Lexical Environment):存储函数声明和 var 声明的变量
  • 外部环境引用(Outer Environment Reference):指向外层词法环境的引用

闭包正是通过这个”外部环境引用”来访问外层作用域的变量。


闭包的常见应用场景

1. 数据私有化

闭包可以用来创建私有变量,这是模块模式的基础:

function createCounter() {
  let count = 0; // 私有变量
  
  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount());  // 2
// 无法直接访问 count 变量

2. 函数工厂

闭包可以用来创建带有预设参数的函数:

function multiply(factor) {
  return function(number) {
    return number * factor;
  };
}

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

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

3. 事件处理与回调

在异步编程中,闭包帮助我们保存状态:

function setupButton(buttonId, message) {
  const button = document.getElementById(buttonId);
  
  button.addEventListener('click', function() {
    // 这个回调函数是一个闭包,捕获了 message 变量
    alert(message);
  });
}

setupButton('btn1', '按钮一被点击');
setupButton('btn2', '按钮二被点击');

4. 循环中的闭包

经典的面试题——循环中的闭包问题:

// 问题:使用 var 时,所有回调都输出 5
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 输出 5 次 5
  }, 100);
}

// 解法 1:使用 IIFE 创建独立作用域
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j); // 0, 1, 2, 3, 4
    }, 100);
  })(i);
}

// 解法 2:使用 let(推荐)
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 0, 1, 2, 3, 4
  }, 100);
}

闭包与内存管理

内存泄漏风险

闭包会持有对外部变量的引用,如果不小心使用,可能导致内存泄漏:

function createLargeData() {
  const largeData = new Array(1000000).fill('data');
  
  return function() {
    console.log('closure still holds largeData');
    // 即使这里没有使用 largeData,它也无法被 GC 回收
  };
}

const fn = createLargeData();
// largeData 仍然存在于内存中

最佳实践

  1. 及时释放:不再需要的闭包,将引用设为 null
  2. 只捕获需要的变量:避免捕获整个大对象
  3. 使用 WeakMap/WeakSet:对于需要缓存但不想阻止 GC 的场景
function createProcessor(data) {
  // 只捕获需要的部分
  const id = data.id;
  
  return {
    getId: () => id,
    process: () => {
      // 使用 data 进行处理
      return data.value * 2;
    }
  };
}

闭包在框架中的应用

React Hooks

React 的 useEffectuseCallback 等 Hook 都依赖闭包来捕获状态:

function Counter() {
  const [count, setCount] = useState(0);
  
  // 这个 effect 捕获了 count 变量
  useEffect(() => {
    document.title = `点击了 ${count} 次`;
  }, [count]);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      点击了 {count} 次
    </button>
  );
}

Vue Composition API

Vue 3 的 setup 函数同样利用闭包:

import { ref, onMounted } from 'vue';

export default {
  setup() {
    const count = ref(0);
    
    // increment 是一个闭包,捕获了 count
    function increment() {
      count.value++;
    }
    
    onMounted(() => {
      console.log('组件已挂载');
    });
    
    return { count, increment };
  }
};

总结

闭包是 JavaScript 中一个强大且基础的特性。理解闭包的关键在于:

  1. 词法作用域:函数的作用域在定义时确定
  2. 作用域链:函数可以访问其外层作用域的变量
  3. 变量持有:闭包让变量在函数执行完毕后仍然存活

掌握闭包不仅能帮你写出更优雅的代码,还能让你更深入地理解 JavaScript 的运行机制。它是通往高级 JavaScript 开发的必经之路。

JavaScript 闭包 前端 核心概念