> 技术文档 > 【闭包】前端的“保护神”——闭包详解+底层原理

【闭包】前端的“保护神”——闭包详解+底层原理

目录

 一、闭包是什么?概念

二、闭包为什么存在?作用

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 函数中,外部无法直接访问和修改它,只有通过 incrementdecrement 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 函数的作用域内被封装,外部无法直接访问或修改它,只有通过 depositwithdraw 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 是一个私有变量,只能通过 incrementdecrement 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);

上述执行的过程中到底在做什么:

  1. A(2) 的调用:

    • JavaScript 引擎会为 A(2) 创建一个 执行上下文。
    • 当调用 A(2) 时,y 赋值为 2,并且在 A 内部创建了一个局部变量 x = 2 和一个函数 B(z)
    • 然后,A 返回了函数 B
  2. 形成闭包:

    • 函数 BA 内部定义,因此它形成了闭包,能够访问 A 内部的变量 xy,即使 A 执行完毕,B 仍然可以访问这些变量。
    • A(2) 执行完,JavaScript 会销毁 A 的执行上下文,但由于 B 是通过闭包持有对 A 作用域的引用,因此 x y 并没有被销毁,它们的内存空间会保留下来。
  3. B 赋值给 C:

    • 通过 let C = A(2);,变量 C 被赋值为函数 B,且 C 具有 A 中的作用域(闭包),能够访问 xy
  4. 调用 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 的内存空间

评论区做答。

订阅《前端通过之路》,助你一路通关!