【闭包】前端的“保护神”——闭包详解+底层原理
目录
一、闭包是什么?概念
二、闭包为什么存在?作用
1. 创建私有变量
2. 实现数据封装与信息隐藏
3. 模拟私有方法
4. 保存函数执行时的状态
5. 回调函数和事件处理
6. 模块化编程
7. 懒加载与延迟执行
三、闭包怎么用?实践+业务场景
1. 封装私有变量
2. 延迟执行(定时器、异步回调)
3. 事件监听和回调函数
5. 防抖和节流
业务场景:权限控制和角色管理
四、深入底层了解闭包的运行原理(难度指数⭐⭐⭐⭐)
一、闭包是什么?概念
闭包是指 函数可以“记住”并访问定义时的作用域,即使这个函数在外部被调用时,依然能访问到其定义时的父函数的局部变量。
- 
父函数和子函数:
- 闭包通常发生在一个函数(父函数)内部定义了另一个函数(子函数),且子函数可以访问父函数的局部变量。
 
 - 
通过
return暴露子函数:- 当父函数返回子函数时,子函数就形成了闭包。因为子函数不仅仅是返回的函数,它还“记住”了父函数的作用域。
 
 - 
作用域链和内存管理:
- 通常,父函数的局部变量在父函数执行完毕后会被销毁,但由于闭包的存在,这些局部变量会被保留在内存中,直到闭包不再被引用。
 - 闭包使得父函数的局部变量不被销毁,同时也避免了全局作用域的污染,因为它们只在闭包内部可见。
 
 
二、闭包为什么存在?作用
1. 创建私有变量
闭包最常见的作用之一是实现 私有变量。在 JavaScript 中,变量通常是公开的,任何函数都能访问它们。而闭包允许我们创建只能通过特定函数访问的私有变量,这样就可以避免外部代码随意访问或修改它们。
- 示例:
 
function createCounter() { let count = 0; // 这是一个私有变量 return { increment: function() { count++; console.log(count); }, decrement: function() { count--; console.log(count); }, getCount: function() { return count; } };}const counter = createCounter();counter.increment(); // 输出: 1counter.increment(); // 输出: 2console.log(counter.getCount()); // 输出: 2// count 变量是私有的,外部无法直接访问
在这个例子中,count 变量通过闭包被封装在 createCounter 函数中,外部无法直接访问和修改它,只有通过 increment、decrement 和 getCount 方法才能操作它。
2. 实现数据封装与信息隐藏
闭包提供了数据封装的能力,可以将状态和行为封装在一个函数内部,并通过暴露的接口与外部进行交互。这有助于信息隐藏,防止外部代码不小心或恶意地修改内部数据。
- 
示例:
 
function bankAccount(initialBalance) { let balance = initialBalance; // 私有变量 return { deposit: function(amount) { balance += amount; console.log(`Deposited: $${amount}`); }, withdraw: function(amount) { if (balance >= amount) { balance -= amount; console.log(`Withdrew: $${amount}`); } else { console.log(\'Insufficient funds\'); } }, getBalance: function() { return balance; } };}const myAccount = bankAccount(1000);myAccount.deposit(500); // Deposited: $500myAccount.withdraw(200); // Withdrew: $200console.log(myAccount.getBalance()); // 1300// 不能直接访问或修改 balance
这里的 balance 变量在 bankAccount 函数的作用域内被封装,外部无法直接访问或修改它,只有通过 deposit、withdraw 和 getBalance 方法才能与其交互。
3. 模拟私有方法
除了私有变量,闭包也可以用来模拟 私有方法。你可以将某些功能封装在闭包内部,外部只能通过公开的方法调用它们,从而达到隐藏细节、减少外部依赖的目的。
- 
示例:
 
function car(model) { let speed = 0; // 私有变量 function accelerate() { speed += 10; console.log(`Accelerating... Speed is now ${speed} km/h`); } return { start: function() { console.log(`${model} is starting`); accelerate(); } };}const myCar = car(\'Toyota\');myCar.start(); // Toyota is starting  // Accelerating... Speed is now 10 km/h
在这个例子中,accelerate 函数是私有的,外部无法直接调用它,只有通过 start 方法间接调用。
4. 保存函数执行时的状态
闭包能够保持其外部函数的执行上下文,即使外部函数已经执行完毕。这样,我们可以保存函数的 状态,在后续的调用中继续使用这些状态。这对于处理 异步操作 或 回调函数 中的状态非常有用。
- 示例:
 
function makeAdder(x) { return function(y) { return x + y; // 闭包可以记住 x 的值 };}const add5 = makeAdder(5);console.log(add5(10)); // 15const add10 = makeAdder(10);console.log(add10(10)); // 20
在这个例子中,makeAdder 返回的函数是一个闭包,它“记住”了 x 的值。即使 makeAdder 执行结束后,x 仍然在闭包中保存,并且在后续的调用中可以使用它。
5. 回调函数和事件处理
在前端开发中,闭包广泛应用于 事件处理 和 异步回调。它们能够保持对外部数据(如事件触发时的状态、函数参数等)的访问,即使在异步操作完成后,闭包仍然能够访问这些数据。
- 示例:事件处理中的闭包
 
function setupButton() { let counter = 0; // 闭包中的私有状态 document.getElementById(\'myButton\').addEventListener(\'click\', function() { counter++; console.log(`Button clicked ${counter} times`); });}setupButton();
在这个例子中,事件回调函数可以访问 counter 变量,它即使在 setupButton 函数执行完毕后仍然保持状态。
6. 模块化编程
闭包帮助我们将代码分成独立的模块,每个模块有自己的私有数据和方法。这样不仅可以避免全局命名冲突,还可以提高代码的可维护性和可复用性。
- 示例:
 
const counterModule = (function() { let count = 0; // 私有变量 return { increment: function() { count++; console.log(count); }, decrement: function() { count--; console.log(count); } };})();counterModule.increment(); // 1counterModule.decrement(); // 0
通过立即执行函数表达式(IIFE),counterModule 模块中的 count 是私有的,外部无法直接访问。闭包保证了每个模块都有独立的作用域和私有数据。
7. 懒加载与延迟执行
闭包还可以用于延迟执行函数和延迟计算,常见于懒加载场景。例如,某些数据或资源的加载操作可以通过闭包延迟到需要时再执行。
- 
示例:
 
function fetchData() { let data = null; return function() { if (data === null) { console.log(\'Fetching data...\'); data = \'Some data\'; // 模拟数据加载 } return data; };}const getData = fetchData();console.log(getData()); // Fetching data... Some dataconsole.log(getData()); // Some data
这里,data 只在第一次调用 getData() 时被加载,之后就不会再进行加载操作,闭包保存了 data 的状态。
三、闭包怎么用?实践+业务场景
1. 封装私有变量
闭包常常用于封装私有变量和创建数据的封装(即模块化编程)。在 JavaScript 中,通常没有内建的私有变量机制,但闭包可以帮助你达到类似的效果。
- 示例:计数器
 
function createCounter() { let count = 0; // 私有变量 return { increment: function() { count++; console.log(count); }, decrement: function() { count--; console.log(count); }, getCount: function() { return count; } };}const counter = createCounter();counter.increment(); // 输出: 1counter.increment(); // 输出: 2counter.decrement(); // 输出: 1console.log(counter.getCount()); // 输出: 1
解析:
count是一个私有变量,只能通过increment、decrement和getCount方法访问。- 外部无法直接访问 
count,实现了数据的封装。 
2. 延迟执行(定时器、异步回调)
闭包经常用于处理异步操作和定时任务。例如,使用 setTimeout 或 setInterval 时,闭包允许你保留函数的执行上下文,从而延迟执行某些操作。
- 
示例:延迟执行任务
 
function createDelayedTask(message, delay) { return function() { setTimeout(function() { console.log(message); }, delay); };}const delayedTask = createDelayedTask(\'Hello, World!\', 2000);delayedTask(); // 2秒后输出: Hello, World!
解析:
createDelayedTask返回一个闭包,这个闭包可以记住其外部环境中的变量(如message和delay)。- 通过 
setTimeout延迟输出message,即使函数createDelayedTask已经执行完毕。 
3. 事件监听和回调函数
闭包在事件监听器和回调函数中非常常见。它可以让回调函数访问外部作用域中的变量,从而保持对数据的引用。
5. 防抖和节流
防抖和节流是常见的性能优化技巧。防抖(Debouncing)通常用于限制某些操作频繁触发(如输入框中的搜索建议),而节流(Throttling)则是控制某些操作的触发频率(如窗口大小调整事件)。
业务场景:权限控制和角色管理
闭包可以用于权限管理和角色管理的场景中,通过闭包来封装不同角色的权限信息,从而提供灵活的权限控制。
- 示例:权限管理
 
function createRoleChecker(role) { const permissions = { admin: [\'read\', \'write\', \'delete\'], user: [\'read\'], guest: [] }; return function(permission) { if (permissions[role] && permissions[role].includes(permission)) { console.log(`${role} has ${permission} permission.`); } else { console.log(`${role} does not have ${permission} permission.`); } };}const adminChecker = createRoleChecker(\'admin\');const userChecker = createRoleChecker(\'user\');const guestChecker = createRoleChecker(\'guest\');adminChecker(\'write\'); // admin has write permission.userChecker(\'write\'); // user does not have write permission.guestChecker(\'read\'); // guest does not have read permission.
解释:
createRoleChecker返回一个闭包,它保存了角色的权限信息。- 每次调用 
roleChecker时,可以判断特定角色是否拥有某个权限。 - 通过闭包,你可以灵活地管理角色和权限数据,避免权限数据暴露。
 
四、深入底层了解闭包的运行原理(难度指数⭐⭐⭐⭐)
思考:
- 下面代码输出什么?
 - 当 
A(2)执行时,局部变量x和y存储在内存中,它们什么时候会被销毁? 
function A(y) { let x = 2; function B(z) { console.log(x + y + z); } return B;}let C = A(2);C(3);
上述执行的过程中到底在做什么:
- 
A(2)的调用:- JavaScript 引擎会为 
A(2)创建一个 执行上下文。 - 当调用 
A(2)时,y赋值为2,并且在A内部创建了一个局部变量x = 2和一个函数B(z)。 - 然后,
A返回了函数B。 
 - JavaScript 引擎会为 
 - 
形成闭包:
- 函数 
B在A内部定义,因此它形成了闭包,能够访问A内部的变量x和y,即使A执行完毕,B仍然可以访问这些变量。 A(2)执行完,JavaScript 会销毁A的执行上下文,但由于B是通过闭包持有对A作用域的引用,因此x和y并没有被销毁,它们的内存空间会保留下来。
 - 函数 
 - 
将
B赋值给C:- 通过 
let C = A(2);,变量C被赋值为函数B,且C具有A中的作用域(闭包),能够访问x和y。 
 - 通过 
 - 
调用
C(3):- 当调用 
C(3)时,实际执行的是B(3)。JavaScript 会创建C(3)的执行上下文:即B(3)的执行上下文。 - 在 
B中,x和y来自A的作用域,z来自B的参数。因此,x + y + z被计算为7,并打印出来。 C(3)调用结束,C(3)的执行上下文即B(3)的执行上下文会被销毁。 但闭包仍然存在,因为B被保存在变量C中,并且C仍然引用着闭包。当C或B被垃圾回收时,闭包才会被销毁。因此,在C(3)执行后,虽然B的执行上下文栈帧被销毁,但闭包中的内存(如x和y)会继续存在,直到C不再引用B。
 - 当调用 
 
参考【易混概念】执行上下文和内存空间的联系区别

留作业:如果闭包
B被赋值给多个其他变量,这些变量会如何影响x和y的内存空间?评论区做答。
订阅《前端通过之路》,助你一路通关!


