Skip to content

闭包:JavaScript 最强大的特性之一

闭包是什么

想象你正在参观一个艺术展览。展览结束后,你带回家一张参观纪念册。这本纪念册不仅记录了展览的内容,还保留了观展时的环境信息——展厅的布局、当时的光线、甚至是讲解员的声音。即使展览已经结束,展厅已经改作他用,但通过这本纪念册,你仍然可以"访问"那个特定时刻的展览环境。

JavaScript 中的闭包(Closure)就像这本纪念册。它是一个函数以及该函数被创建时所处环境的组合。即使创建闭包的外部函数已经执行完毕,闭包仍然可以访问那个函数作用域中的变量。

闭包的技术定义

从技术角度来说,闭包是指:

  1. 一个函数
  2. 加上该函数能够访问的所有外部作用域中的变量

每当 JavaScript 中创建一个函数时,闭包就会在函数创建的同时被创建出来。这个特性让函数能够"记住"并访问其词法作用域,即使该函数在其词法作用域之外执行。

闭包的基本示例

让我们从最简单的例子开始理解闭包:

javascript
function createGreeting(greeting) {
  // greeting 是外部函数的参数

  // 返回的这个函数就是一个闭包
  return function (name) {
    console.log(`${greeting}, ${name}!`);
  };
}

const sayHello = createGreeting("Hello");
const sayHi = createGreeting("Hi");

sayHello("Sarah"); // "Hello, Sarah!"
sayHello("Michael"); // "Hello, Michael!"

sayHi("Sarah"); // "Hi, Sarah!"
sayHi("Michael"); // "Hi, Michael!"

// 虽然 createGreeting 已经执行完毕
// 但返回的函数仍然可以访问 greeting 变量

在这个例子中,sayHellosayHi 都是闭包。它们不仅是函数本身,还包含了各自创建时的环境:sayHello 记住了 greeting 是 "Hello",而 sayHi 记住了 greeting 是 "Hi"。

闭包如何工作

当我们调用 createGreeting("Hello") 时,发生了以下过程:

  1. 函数执行createGreeting 函数执行,创建了一个新的执行上下文
  2. 变量创建:在这个执行上下文中,greeting 参数被赋值为 "Hello"
  3. 返回函数:返回一个新函数(暂且叫它 innerFunc
  4. 形成闭包innerFunc 被返回时,它携带了对 createGreeting 执行上下文的引用
  5. 保持引用:虽然 createGreeting 执行完毕,但因为 innerFunc 仍然引用着 greeting,所以这个变量不会被垃圾回收
  6. 访问变量:当我们调用 sayHello("Sarah") 时,内部函数通过闭包访问到了 greeting 变量

闭包的核心特性

数据封装和私有变量

闭包最强大的应用之一是实现数据封装,创建真正的私有变量:

javascript
function createBankAccount(initialBalance) {
  // 私有变量 - 外部无法直接访问
  let balance = initialBalance;
  const transactionHistory = [];

  // 返回公共接口
  return {
    deposit(amount) {
      if (amount > 0) {
        balance += amount;
        transactionHistory.push({
          type: "deposit",
          amount: amount,
          timestamp: new Date(),
          balance: balance,
        });
        return balance;
      }
      throw new Error("Deposit amount must be positive");
    },

    withdraw(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        transactionHistory.push({
          type: "withdraw",
          amount: amount,
          timestamp: new Date(),
          balance: balance,
        });
        return balance;
      }
      throw new Error("Invalid withdrawal amount");
    },

    getBalance() {
      return balance;
    },

    getTransactionHistory() {
      // 返回副本,防止外部修改
      return [...transactionHistory];
    },
  };
}

const myAccount = createBankAccount(1000);

myAccount.deposit(500);
console.log(myAccount.getBalance()); // 1500

myAccount.withdraw(200);
console.log(myAccount.getBalance()); // 1300

// 无法直接访问私有变量
console.log(myAccount.balance); // undefined
console.log(myAccount.transactionHistory); // undefined

// 只能通过公共接口访问
console.log(myAccount.getTransactionHistory());
// [{ type: 'deposit', amount: 500, ... }, { type: 'withdraw', amount: 200, ... }]

在这个例子中,balancetransactionHistory 是完全私有的。外部代码无法直接访问或修改它们,只能通过提供的公共方法进行操作。这就是闭包实现的数据封装。

创建函数工厂

闭包可以用来创建定制化的函数:

javascript
function createMultiplier(multiplier) {
  return function (number) {
    return number * multiplier;
  };
}

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

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

// 每个函数都记住了自己的 multiplier
console.log(double(8)); // 16
console.log(triple(8)); // 24
console.log(tenTimes(8)); // 80

保持状态

闭包可以在函数调用之间保持状态:

javascript
function createCounter() {
  let count = 0;
  let history = [];

  return {
    increment() {
      count++;
      history.push({ action: "increment", value: count, time: Date.now() });
      return count;
    },

    decrement() {
      count--;
      history.push({ action: "decrement", value: count, time: Date.now() });
      return count;
    },

    reset() {
      count = 0;
      history.push({ action: "reset", value: count, time: Date.now() });
      return count;
    },

    getValue() {
      return count;
    },

    getHistory() {
      return [...history];
    },
  };
}

const counter = createCounter();

counter.increment(); // 1
counter.increment(); // 2
counter.increment(); // 3
counter.decrement(); // 2

console.log(counter.getValue()); // 2
console.log(counter.getHistory());
// [
//   { action: 'increment', value: 1, time: ... },
//   { action: 'increment', value: 2, time: ... },
//   { action: 'increment', value: 3, time: ... },
//   { action: 'decrement', value: 2, time: ... }
// ]

闭包的实用模式

模块模式

使用闭包创建模块,实现命名空间和私有成员:

javascript
const TaskManager = (function () {
  // 私有变量和方法
  let tasks = [];
  let nextId = 1;

  function findTaskById(id) {
    return tasks.find((task) => task.id === id);
  }

  function validateTask(task) {
    if (!task.title || task.title.trim() === "") {
      throw new Error("Task must have a title");
    }
    if (task.priority && !["low", "medium", "high"].includes(task.priority)) {
      throw new Error("Invalid priority level");
    }
  }

  // 公共接口
  return {
    addTask(taskData) {
      const task = {
        id: nextId++,
        title: taskData.title,
        description: taskData.description || "",
        priority: taskData.priority || "medium",
        completed: false,
        createdAt: new Date(),
      };

      validateTask(task);
      tasks.push(task);

      return task.id;
    },

    completeTask(id) {
      const task = findTaskById(id);
      if (task) {
        task.completed = true;
        task.completedAt = new Date();
        return true;
      }
      return false;
    },

    deleteTask(id) {
      const index = tasks.findIndex((task) => task.id === id);
      if (index !== -1) {
        tasks.splice(index, 1);
        return true;
      }
      return false;
    },

    getTasks(filter = {}) {
      let filteredTasks = [...tasks];

      if (filter.completed !== undefined) {
        filteredTasks = filteredTasks.filter(
          (task) => task.completed === filter.completed
        );
      }

      if (filter.priority) {
        filteredTasks = filteredTasks.filter(
          (task) => task.priority === filter.priority
        );
      }

      return filteredTasks;
    },

    getTaskCount() {
      return {
        total: tasks.length,
        completed: tasks.filter((t) => t.completed).length,
        pending: tasks.filter((t) => !t.completed).length,
      };
    },
  };
})();

// 使用模块
const taskId = TaskManager.addTask({
  title: "Complete project documentation",
  priority: "high",
});

TaskManager.addTask({
  title: "Review pull requests",
  priority: "medium",
});

console.log(TaskManager.getTaskCount());
// { total: 2, completed: 0, pending: 2 }

TaskManager.completeTask(taskId);

console.log(TaskManager.getTasks({ completed: false }));
// [{ id: 2, title: "Review pull requests", ... }]

// 私有变量无法访问
// console.log(tasks); // ReferenceError
// TaskManager.nextId; // undefined

柯里化和部分应用

使用闭包实现函数柯里化:

javascript
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }

    return function (...nextArgs) {
      return curried.apply(this, args.concat(nextArgs));
    };
  };
}

// 原始函数
function calculatePrice(basePrice, taxRate, discount) {
  return basePrice * (1 + taxRate) * (1 - discount);
}

// 柯里化后的函数
const curriedPrice = curry(calculatePrice);

// 创建特定税率的价格计算器
const priceWithTax = curriedPrice(100)(0.1);
console.log(priceWithTax(0)); // 110 (无折扣)
console.log(priceWithTax(0.1)); // 99 (10%折扣)
console.log(priceWithTax(0.2)); // 88 (20%折扣)

// 创建特定场景的计算器
const regularCustomerPrice = curriedPrice(100)(0.1)(0.05);
const vipCustomerPrice = curriedPrice(100)(0.1)(0.15);

console.log(regularCustomerPrice); // 104.5
console.log(vipCustomerPrice); // 93.5

事件处理器和回调

闭包在事件处理中特别有用:

javascript
function createButtonHandler(buttonId, actionName) {
  let clickCount = 0;
  const createdAt = Date.now();

  return function (event) {
    clickCount++;

    const timeSinceCreation = Date.now() - createdAt;

    console.log(`Button ${buttonId} (${actionName})`);
    console.log(`Clicked ${clickCount} times`);
    console.log(`Created ${timeSinceCreation}ms ago`);

    // 可以访问事件对象、外部变量和参数
    console.log(`Event type: ${event.type}`);
  };
}

// 模拟按钮点击
const saveHandler = createButtonHandler("btn-save", "Save Document");
const submitHandler = createButtonHandler("btn-submit", "Submit Form");

// 每个处理器维护自己的状态
saveHandler({ type: "click" });
// Button btn-save (Save Document)
// Clicked 1 times
// Created 0ms ago

saveHandler({ type: "click" });
// Clicked 2 times

submitHandler({ type: "click" });
// Button btn-submit (Submit Form)
// Clicked 1 times

延迟执行和记忆化

使用闭包实现函数结果的缓存:

javascript
function memoize(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log("从缓存返回结果");
      return cache.get(key);
    }

    console.log("计算新结果");
    const result = fn.apply(this, args);
    cache.set(key, result);

    return result;
  };
}

// 创建一个耗时的函数
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// 创建记忆化版本
const memoizedFib = memoize(function fibonacci(n) {
  if (n <= 1) return n;
  return memoizedFib(n - 1) + memoizedFib(n - 2);
});

console.log(memoizedFib(10)); // 计算新结果
console.log(memoizedFib(10)); // 从缓存返回结果
console.log(memoizedFib(11)); // 只需计算 fib(11),其他都从缓存获取

闭包的常见陷阱

循环中的闭包

这是 JavaScript 中最著名的陷阱之一:

javascript
// 问题代码
function createButtons() {
  const buttons = [];

  for (var i = 0; i < 3; i++) {
    buttons.push(function () {
      console.log(`Button ${i} clicked`);
    });
  }

  return buttons;
}

const buttons = createButtons();
buttons[0](); // "Button 3 clicked" (预期是 0)
buttons[1](); // "Button 3 clicked" (预期是 1)
buttons[2](); // "Button 3 clicked" (预期是 2)

// 原因:所有闭包共享同一个 i 变量
// 当函数执行时,循环已结束,i 的值是 3

解决方案:

javascript
// 解决方案1:使用 let 创建块级作用域
function createButtonsFixed1() {
  const buttons = [];

  for (let i = 0; i < 3; i++) {
    buttons.push(function () {
      console.log(`Button ${i} clicked`);
    });
  }

  return buttons;
}

// 解决方案2:使用 IIFE 创建新作用域
function createButtonsFixed2() {
  const buttons = [];

  for (var i = 0; i < 3; i++) {
    (function (index) {
      buttons.push(function () {
        console.log(`Button ${index} clicked`);
      });
    })(i);
  }

  return buttons;
}

// 解决方案3:使用函数参数
function createButtonsFixed3() {
  const buttons = [];

  for (var i = 0; i < 3; i++) {
    buttons.push(
      (function (index) {
        return function () {
          console.log(`Button ${index} clicked`);
        };
      })(i)
    );
  }

  return buttons;
}

const fixedButtons = createButtonsFixed1();
fixedButtons[0](); // "Button 0 clicked" ✓
fixedButtons[1](); // "Button 1 clicked" ✓
fixedButtons[2](); // "Button 2 clicked" ✓

过度使用导致的内存问题

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

javascript
// 潜在的内存问题
function createEventHandler() {
  const largeData = new Array(1000000).fill("some data");

  return function handleEvent(event) {
    // 即使不使用 largeData,它也会被保留在内存中
    console.log("Event handled");
  };
}

// 更好的做法:只保留需要的数据
function createEventHandlerOptimized() {
  const largeData = new Array(1000000).fill("some data");
  const summary = `Data size: ${largeData.length}`;

  // 不再引用 largeData,它可以被垃圾回收
  return function handleEvent(event) {
    console.log(summary);
  };
}

this 指向问题

闭包中的 this 可能不会按预期工作:

javascript
const user = {
  name: "Sarah",
  tasks: ["Task 1", "Task 2", "Task 3"],

  // 问题代码
  showTasksBroken() {
    this.tasks.forEach(function (task) {
      // this 指向 undefined 或全局对象,而不是 user
      console.log(`${this.name}: ${task}`);
    });
  },

  // 解决方案1:使用箭头函数
  showTasksArrow() {
    this.tasks.forEach((task) => {
      // 箭头函数继承外部的 this
      console.log(`${this.name}: ${task}`);
    });
  },

  // 解决方案2:使用闭包保存 this
  showTasksClosure() {
    const self = this;
    this.tasks.forEach(function (task) {
      console.log(`${self.name}: ${task}`);
    });
  },

  // 解决方案3:使用 bind
  showTasksBind() {
    this.tasks.forEach(
      function (task) {
        console.log(`${this.name}: ${task}`);
      }.bind(this)
    );
  },
};

user.showTasksArrow();
// Sarah: Task 1
// Sarah: Task 2
// Sarah: Task 3

闭包的性能考虑

内存占用

每个闭包都会保持对其词法环境的引用,这可能增加内存占用:

javascript
// 不好的做法 - 创建很多不必要的闭包
function inefficientCode() {
  const data = new Array(10000).fill(0);

  // 每次调用都创建新闭包
  return {
    method1: function () {
      return data[0];
    },
    method2: function () {
      return data[1];
    },
    method3: function () {
      return data[2];
    },
    // ... 更多方法
  };
}

// 更好的做法 - 共享方法
function efficientCode() {
  const data = new Array(10000).fill(0);

  // 方法在原型上,共享而不是每次都创建
  const api = Object.create({
    get(index) {
      return data[index];
    },
    set(index, value) {
      data[index] = value;
    },
  });

  return api;
}

避免不必要的闭包

javascript
// 不必要的闭包
function processItems(items) {
  const multiplier = 2;

  // 这个闭包捕获了 multiplier,但其实可以改为参数
  return items.map(function (item) {
    return item * multiplier;
  });
}

// 更好的做法
function double(item) {
  return item * 2;
}

function processItemsOptimized(items) {
  // 重用同一个函数,不创建新闭包
  return items.map(double);
}

实际应用场景

创建私有 API

javascript
function createAPI(apiKey) {
  // 私有配置
  const config = {
    baseURL: "https://api.example.com",
    timeout: 5000,
    key: apiKey,
  };

  // 私有辅助函数
  function buildHeaders() {
    return {
      Authorization: `Bearer ${config.key}`,
      "Content-Type": "application/json",
    };
  }

  function handleError(error) {
    console.error("API Error:", error);
    throw error;
  }

  // 公共接口
  return {
    async get(endpoint) {
      try {
        const response = await fetch(`${config.baseURL}${endpoint}`, {
          headers: buildHeaders(),
          timeout: config.timeout,
        });
        return await response.json();
      } catch (error) {
        handleError(error);
      }
    },

    async post(endpoint, data) {
      try {
        const response = await fetch(`${config.baseURL}${endpoint}`, {
          method: "POST",
          headers: buildHeaders(),
          body: JSON.stringify(data),
          timeout: config.timeout,
        });
        return await response.json();
      } catch (error) {
        handleError(error);
      }
    },
  };
}

const api = createAPI("my-secret-key");

// 使用 API,但无法访问 apiKey 或其他私有成员
api.get("/users");
api.post("/users", { name: "John" });

// 这些都无法访问
// console.log(api.config);      // undefined
// console.log(api.buildHeaders); // undefined

函数防抖和节流

javascript
function debounce(func, delay) {
  let timeoutId;

  return function (...args) {
    // 清除之前的定时器
    clearTimeout(timeoutId);

    // 设置新的定时器
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

function throttle(func, limit) {
  let inThrottle;

  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;

      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// 使用示例
const handleSearch = debounce(function (query) {
  console.log(`Searching for: ${query}`);
}, 300);

const handleScroll = throttle(function (event) {
  console.log("Scroll event handled");
}, 100);

// 模拟事件
handleSearch("javascript"); // 只在停止输入300ms后执行
handleSearch("closure");
handleSearch("tutorial");

handleScroll(); // 立即执行
handleScroll(); // 被忽略
// ... 100ms后才能再次执行

总结

闭包是 JavaScript 中最强大也最优雅的特性之一。理解并掌握闭包,对于编写高质量的 JavaScript 代码至关重要:

  1. 核心概念:闭包是函数和其词法环境的组合,能够访问外部作用域的变量
  2. 主要用途:数据封装、创建私有变量、函数工厂、状态保持
  3. 实用模式:模块模式、柯里化、事件处理、记忆化
  4. 常见陷阱:循环中的闭包、内存泄漏、this 指向问题
  5. 性能考虑:避免过度使用、注意内存占用、合理管理作用域链

掌握闭包不仅能让你的代码更加优雅和强大,还能帮助你理解许多 JavaScript 框架和库的内部实现原理。在下一篇文章中,我们将探讨闭包的各种应用模式,看看如何在实际项目中充分利用这个强大的特性。