目录
一、闭包是什么?概念
闭包是指 函数可以“记住”并访问定义时的作用域,即使这个函数在外部被调用时,依然能访问到其定义时的父函数的局部变量。
-
父函数和子函数:
- 闭包通常发生在一个函数(父函数)内部定义了另一个函数(子函数),且子函数可以访问父函数的局部变量。
-
通过
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
函数中,外部无法直接访问和修改它,只有通过 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: $500
myAccount.withdraw(200); // Withdrew: $200
console.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)); // 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
是一个私有变量,只能通过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
。
- 当调用
标签:闭包,function,保护神,函数,count,详解,console,log From: https://blog.csdn.net/weixin_45188218/article/details/144620085留作业:如果闭包
B
被赋值给多个其他变量,这些变量会如何影响x
和y
的内存空间?评论区做答。