闭包:JavaScript 最强大的特性之一
闭包是什么
想象你正在参观一个艺术展览。展览结束后,你带回家一张参观纪念册。这本纪念册不仅记录了展览的内容,还保留了观展时的环境信息——展厅的布局、当时的光线、甚至是讲解员的声音。即使展览已经结束,展厅已经改作他用,但通过这本纪念册,你仍然可以"访问"那个特定时刻的展览环境。
JavaScript 中的闭包(Closure)就像这本纪念册。它是一个函数以及该函数被创建时所处环境的组合。即使创建闭包的外部函数已经执行完毕,闭包仍然可以访问那个函数作用域中的变量。
闭包的技术定义
从技术角度来说,闭包是指:
- 一个函数
- 加上该函数能够访问的所有外部作用域中的变量
每当 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 变量在这个例子中,sayHello 和 sayHi 都是闭包。它们不仅是函数本身,还包含了各自创建时的环境:sayHello 记住了 greeting 是 "Hello",而 sayHi 记住了 greeting 是 "Hi"。
闭包如何工作
当我们调用 createGreeting("Hello") 时,发生了以下过程:
- 函数执行:
createGreeting函数执行,创建了一个新的执行上下文 - 变量创建:在这个执行上下文中,
greeting参数被赋值为 "Hello" - 返回函数:返回一个新函数(暂且叫它
innerFunc) - 形成闭包:
innerFunc被返回时,它携带了对createGreeting执行上下文的引用 - 保持引用:虽然
createGreeting执行完毕,但因为innerFunc仍然引用着greeting,所以这个变量不会被垃圾回收 - 访问变量:当我们调用
sayHello("Sarah")时,内部函数通过闭包访问到了greeting变量
闭包的核心特性
数据封装和私有变量
闭包最强大的应用之一是实现数据封装,创建真正的私有变量:
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, ... }]在这个例子中,balance 和 transactionHistory 是完全私有的。外部代码无法直接访问或修改它们,只能通过提供的公共方法进行操作。这就是闭包实现的数据封装。
创建函数工厂
闭包可以用来创建定制化的函数:
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保持状态
闭包可以在函数调用之间保持状态:
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: ... }
// ]闭包的实用模式
模块模式
使用闭包创建模块,实现命名空间和私有成员:
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柯里化和部分应用
使用闭包实现函数柯里化:
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事件处理器和回调
闭包在事件处理中特别有用:
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延迟执行和记忆化
使用闭包实现函数结果的缓存:
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 中最著名的陷阱之一:
// 问题代码
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解决方案:
// 解决方案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" ✓过度使用导致的内存问题
闭包会保持对外部变量的引用,如果不小心,可能导致内存泄漏:
// 潜在的内存问题
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 可能不会按预期工作:
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闭包的性能考虑
内存占用
每个闭包都会保持对其词法环境的引用,这可能增加内存占用:
// 不好的做法 - 创建很多不必要的闭包
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;
}避免不必要的闭包
// 不必要的闭包
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
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函数防抖和节流
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 代码至关重要:
- 核心概念:闭包是函数和其词法环境的组合,能够访问外部作用域的变量
- 主要用途:数据封装、创建私有变量、函数工厂、状态保持
- 实用模式:模块模式、柯里化、事件处理、记忆化
- 常见陷阱:循环中的闭包、内存泄漏、this 指向问题
- 性能考虑:避免过度使用、注意内存占用、合理管理作用域链
掌握闭包不仅能让你的代码更加优雅和强大,还能帮助你理解许多 JavaScript 框架和库的内部实现原理。在下一篇文章中,我们将探讨闭包的各种应用模式,看看如何在实际项目中充分利用这个强大的特性。