JavaScript中的闭包
并非单一的概念,它涉及到作用域、作用域链、执行上下文、内存管理等多种知识。
如果在一个函数中我们返回了另一个函数,且这个返回的内层函数使用了外层函数的变量,那么外界便能够通过这个返回的函数获取原函数内部的变量值,则我们将返回的函数称为原函数的一个闭包。
这概念看起来是不是有点绕呢??那么我们来看一下下面这个例子:
function outFun(){
let num = 1;
num++;
const innerFun = ()=>{}
return ()=>{
console.log(num);
}
}
const innerFun = outFun();
innerFun(); // 控制台输出2
上面是一个简单的闭包示例,在外部函数outFun
内返回了箭头函数,我们将其赋值给innerFun
,调用innerFun
我们可以访问到outFun
函数内的变量num
,innerFun
就是outFun
的一个闭包。
看了上面的例子想必你对于闭包已经有了基本的了解?
那么就有小伙伴要问了:为什么可以在函数外部访问到outFun
中的局部变量呢?
一般来讲,在一个函数执行完毕后,会从函数执行栈中出栈,函数内的局部变量在下一个垃圾回收(GC)节点会被回收,同时该函数对应的执行上下文会被销毁,所以我们无法在外界访问函数内部定义的变量,也就形成了所谓的函数作用域。
但是我们根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。通过innerFun
访问outFun
中的变量,引用的变量仍然保存在内存中,垃圾回收机制无法将其回收,形成了闭包。
接下来我们通过一道经典面试题来深入了解下闭包:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
上面这道题输出了什么?
答案
Wed Dec 23 2020 22:42:17 GMT+0800 (中国标准时间) 5 Wed Dec 23 2020 22:42:18 GMT+0800 (中国标准时间) 5 Wed Dec 23 2020 22:42:18 GMT+0800 (中国标准时间) 5 Wed Dec 23 2020 22:42:18 GMT+0800 (中国标准时间) 5 Wed Dec 23 2020 22:42:18 GMT+0800 (中国标准时间) 5 Wed Dec 23 2020 22:42:18 GMT+0800 (中国标准时间) 5 因为 var 污染了全局,了解 Event Loop 的同学应该知道在执行上面这段代码时,所有的setTimeout定时任务在for循环执行结束后才会执行,那时候的 i 已经变成了 5 ,而最先输出的是下面的 console.log ,一秒后执行定时任务完成剩余输出。
上面的问题你答对了吗?那么我们进入下一步?
如果我们想要输出5 0 1 2 3 4
,该如何对上面的代码进行改造呢?
熟悉setTimeout
API的小伙伴会给出以下方案:
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(new Date, j);
}, 1000, i);
}
console.log(new Date, i);
setTimeout
的第三个参数为回调函数的传入参数。
熟悉ES6的小伙伴应该会使用let
来实现这个需求吧?
for(let i = 0;i < 5; i++){
setTimeout(function() {
console.log(new Date(), i);
}, 1000)
}
console.log(new Date(), i);
使用let
替代var
,在每一次循环内let
都会形成一个块级作用域,进行重新赋值,但这种解决方案会存在一个问题,因为 i 只会存在于循环内部,所以在外部的 console.log 并无法访问到内部的 i ,这并不算是一个完美的解决方案。
要让外部访问到函数内部的变量,你是不是想到什么了呢?没错!就是我们的主角闭包
?
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(new Date, j);
}, 1000);
})(i);
}
console.log(new Date, i);
利用IIFE (Immediately Invoked Function Expression:声明即执行的函数表达式)
来执行将每一次循环的变量传入定时任务中可以达到我们想要的效果。
如果更进一步,我们不仅想要输出5 0 1 2 3 4
,同时希望它们的输出间隔都为1秒呢?
我们可以修改定时器的时间来实现该需求:
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(new Date, j);
}, 1000 * j);
})(i);
}
setTimeout(function(){
console.log(new Date, i);
}, 1000 * i);
既然我们每一个循环都有一个异步操作,那么我们能不能使用Promise
来实现这个需求呢?
我们可以使用一个数组存放Promise
对象,使用Promise.all
API来实现这个需求:
const arr = [];
for(var i = 0;i < 5;i++){
const getPromise = (i)=>{
return new Promise((resolve)=>{
setTimeout(()=>{
console.log(new Date, i);
resolve();
}, 1000 * i);
})
}
arr.push(getPromise(i));
}
Promise.all(arr)
.then(()=>{
setTimeout(()=>{
console.log(new Date, i);
}, 1000);
})
在 for 循环中使用了var
进行声明而不使用let
进行声明,是因为在执行Promise.all
后之后的回调还需要执行一次定时任务,若使用let
则无法拿到函数作用域内的i
变量,若使用let
最后结果将输入 0 而不是 5 。
既然我们使用了Promise
,那么不妨将其优化一下,不使用Promise.all
而使用async/await
来实现:
const getPromise = (i)=>{
return new Promise((resolve)=>{
setTimeout(()=>{
console.log(new Date, i)
resolve()
}, 1000)
})
}
(async ()=>{
for(var i=0;i<5;i++){
await getPromise(i);
}
await getPromise(i);
})()
这种写法相对来说可读性也有所提高,同时节省了数组所需的内存。
接下来我们回归原点,来看一道与闭包相关的手写代码题:函数柯里化
所谓的柯里化(curry)
就是将接受多个参数的函数通过闭包转化为接收更少参数的函数,该函数返回一个接收剩余参数的函数。柯里化函数能够实现参数的复用。
我们来看一下具体的示例:
function getSum(a, b, c){
return a + b + c;
}
// 柯里化函数
function curryGetSum(a){
return function(b, c){
return a + b + c;
}
}
// 这样也是柯里化函数
function curryGetSum2(a){
return function(b){
return function(C){
return a + b + c;
}
}
}
看到这里你应该能明白什么是柯里化函数了吧?那么接下来让我们一起来尝试实现一个柯里化工具函数吧?
function curry(func){
return function curried(...args){
if(args.length >= func.length){
return func.apply(this,...args);
}else{
return function(...args2){
return curried.apply(this,args.concat(args2))
}
}
}
}
上面的代码结构稍微复杂点,可以代入上面的例子以及实现思路反复咀嚼,能够从根本上理解其实现思路你就赢了?
当然,凡事有好有坏,闭包也不能免俗。闭包在使用的同时存在内存泄漏的风险。
我们来看一下具体的例子:
function first(){
let value = 0;
setInterval(function(){
console.log(value++);
}, 1000)
})
first();
first = null;
使用setInterval
后first
内的value
被引用,即使设置了first = null
内存空间仍然无法释放,每隔一秒仍然会在控制台输出value
值。这种情况需要使用clearInterval
对其清除,占用的内存才能被释放。
const element = document.getElementById('element');
element.innerHTML = '<button id="button">click</button>';
const button = document.getElementById('button');
button.addEventListener('click', function(){
// ...
})
element.innerHTML = '';
使用element.innerHTML = ''
,成功将button
从dom
中移除,但是事件处理函数仍在监听它,element
节点无法回收,内存被占用。这种情况需要使用removeEventListener
函数去除事件监听,防止内存泄漏。
const element = document.getElementById('element');
element.third = "third";
element.parentNode.removeChild(element);
使用element.parentNode.removeChild(element)
将element
从文档流中去除,但是element
仍然存在,该节点所占用的内存无法释放。这种情况需要使用element = null
来释放内存。
到此为此我想你对于闭包造成内存泄漏的情况已经有了基本的认知,闭包虽好,使用的时候也要小心内存泄漏哦?
感谢你花时间读到这里。如果这一篇文章你觉得不错或是对你有所帮助的话,请给笔者一个赞:+1:,如果对文中内容有任何疑问,欢迎评论区留言评论?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!