ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/8/14 0:10:57 拓冰建站 浏览量
【闭包】前端的“保护神”——闭包详解+底层原理

目录

 一、闭包是什么?概念

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

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(); // 输出: 1
counter.increment(); // 输出: 2
console.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: $500
myAccount.withdraw(200); // Withdrew: $200
console.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)); // 15
const 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(); // 1
counterModule.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 data
console.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();  // 输出: 1
counter.increment();  // 输出: 2
counter.decrement();  // 输出: 1
console.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 的内存空间

评论区做答。

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