[JS Daily] JavaScript 闭包原理 - 函数的记忆能力

JavaScript 闭包原理 - 函数的"记忆"能力

闭包(Closure)是 JavaScript 中最核心、也最容易让人困惑的概念之一。很多开发者能写出闭包代码,却不理解背后的原理。今天我们来彻底搞懂它。

什么是闭包?

简单来说,闭包是指函数能够记住并访问它被创建时的作用域,即使该函数在其原始作用域之外执行。

一句话定义

闭包 = 函数 + 函数创建时的词法环境

闭包的核心原理

理解闭包需要先理解 JavaScript 的两个机制:

1. 词法作用域(Lexical Scope)

函数的作用域在函数定义时就确定了,而不是在调用时。这决定了函数能访问哪些变量。

2. 作用域链(Scope Chain)

当访问一个变量时,JavaScript 引擎会从当前作用域开始,逐级向外查找,直到全局作用域。

function createCounter() {
  let count = 0; // 这是一个局部变量

  return function() { // 这个内部函数就是一个闭包
    count++;
    return count;
  };
}

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

在这个例子中,createCounter 执行完毕后,按理说 count 应该被销毁。但因为返回的函数仍然引用着 count,这个变量被"保留"在了闭包中。

闭包的典型应用场景

1. 数据私有化(模拟私有变量)

function createUser(name) {
  let _name = name; // 私有变量

  return {
    getName() { return _name; },
    setName(newName) { _name = newName; }
  };
}

const user = createUser('张三');
console.log(user.getName()); // '张三'
user.setName('李四');
console.log(user.getName()); // '李四'
console.log(user._name); // undefined(无法直接访问)

2. 函数工厂

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

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

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

3. 防抖和节流

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 使用
const handleSearch = debounce((query) => {
  console.log('搜索:', query);
}, 300);

handleSearch('h'); // 不会立即执行
handleSearch('he'); // 取消上一个,重新计时
handleSearch('hello'); // 300ms 后执行一次

4. 模块模式

const counterModule = (function() {
  let count = 0; // 私有变量

  return {
    increment() { count++; },
    decrement() { count--; },
    getCount() { return count; }
  };
})();

counterModule.increment();
console.log(counterModule.getCount()); // 1

闭包的经典陷阱

陷阱1: 循环中的 var

// 错误示例
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出: 3, 3, 3 (而不是 0, 1, 2)

// 正确方式1: 使用 let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出: 0, 1, 2

// 正确方式2: 使用 IIFE
for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// 输出: 0, 1, 2

原因:var 是函数作用域,循环结束后所有回调函数共享同一个 i(值为3)。let 是块级作用域,每次迭代都创建新的绑定。

陷阱2: 内存泄漏

// 潜在的内存泄漏
function setupHandler() {
  const largeData = new Array(1000000).fill('x');

  document.getElementById('btn').addEventListener('click', () => {
    console.log(largeData.length);
  });
}

// 解决方案: 只保留需要的部分
function setupHandler() {
  const largeData = new Array(1000000).fill('x');
  const length = largeData.length; // 只保存需要的值

  document.getElementById('btn').addEventListener('click', () => {
    console.log(length);
  });
}

闭包 vs 对象

什么时候用闭包,什么时候用对象?

场景 闭包 对象
数据量 少量私有数据 大量结构化数据
访问方式 通过方法访问 可直接访问属性
可扩展性 每次创建新闭包 原型共享方法
适用场景 工厂函数、回调、事件处理 数据模型、实体类

闭包的性能考量

闭包会带来额外的内存开销,因为被引用的变量不会被垃圾回收。但这通常不是问题,除非:

  • 创建大量闭包(如循环中)
  • 闭包引用大对象且长期存活
  • 忘记解除事件监听器

最佳实践:闭包使用完后,如果不再需要,将引用设为 null,帮助垃圾回收。

总结

闭包的本质

  • 函数 + 词法环境的组合
  • 让函数"记住"创建时的作用域
  • 实现数据私有化的核心机制

关键要点

  • 理解词法作用域是理解闭包的前提
  • 闭包不是"问题",是特性
  • 合理使用可以写出优雅的代码
  • 滥用可能导致内存问题

—— JavaScript 日更 Day 24 ——

评论

此博客中的热门博文

OpenClaw 救援机器人建设与演进全记录 - 从单点故障到双实例自愈体系

Lossless Claw:无损上下文管理插件分析报告

[Hello-Agents] Day 2: 第一章 初识智能体