什么是闭包?
下面的内容是个人从网上学习并结合自己的理解写下的关于闭包及相关内容的介绍,如有不足或错误之处请指正,谢谢。
前言(遇到的问题)
在大学的一次课程实验作业的编写时,遇到了类似下面的问题:
<html>
<head>
......
</head>
<body>
<div>
<button>1</button>
<button>2</button>
<button>3</button>
</div>
<script>
//获取按钮button的DOM元素
const btn = document.querySelectorAll("button");
//实验要求的是根据不同按钮的值做相应的操作,如删除还是添加
//这里为了更直观看到问题所在,就不详细写实验要求的逻辑代码,直接输出按钮对应的下标i
for(var i=0; i<btn.length; i++){
btn[i].addEventListener("click",() => {
console.log(i);
})
}
</script>
</body>
</html>
这里遇到的问题是:不管点击哪个按钮,都会输出3,而不是我需要的按钮对应的数字(实验要求是对应的操作)
- 这里实际可以在button标签添加onclick属性来绑定操作事件,这也是我当时提交实验作业时用的方法。
- 但这个方法是逃避了上面遇到的问题,并没有解决上面遇到的疑问。
- 只是一个操作的不同解法,而且不够好,因为如果有好多个button,就需要为每个button都添加onclick属性,代码过于冗余。
解决方案
那么有没有什么方法可以还是使用for循环来实现还不出错呢?
两种解决方案:
- 使用
let
来声明i
变量- 使用 闭包 来解决
简单讲一下这里的解决方法1:
- 上面的代码中我们在for循环里使用
var
来声明 i 会导致 变量提升 1。 - 变量提升的
var
所拥有的特性,而JavaScript的ES6语法中新出的let
和const
就没有该特性了。 - 所以我们推荐使用
let
来声明变量 i ,i 在循环块级作用域起作用,它会在每次迭代都声明一个新的变量i存起来。 - 比如:
- 最开始我们点击按钮1,找到事件处理函数,第一次循环迭代新声明一个迭代变量
i=0
,事件处理程序捕获到0; - 点击第2个按钮,再新声明一个迭代变量
i=0
,这个 i 和第一次迭代的变量不同,会加到1,所以事件处理程序捕获到的就是1。
- 最开始我们点击按钮1,找到事件处理函数,第一次循环迭代新声明一个迭代变量
- 而用
var
声明的 i ,因为变量提升,为全局作用域,它在每次迭代是使用同一变量i,所以捕获到的都是同一个i为3
这里解析的可能不是很清楚,该问题涉及到的原因是:“js事件处理器在线程空闲时间不会运行,导致最后运行的时候输出的都是i最后的值”。(网上搜到的解释都是这个,没有具体见解,但我们这篇blog要讲的是闭包,所以就不具体讲,这里先挖个坑,之后的blog再来具体聊聊。点赞助力博主快快更新!)
下面回到主题,在讲闭包是怎么解决该问题的之前先介绍一下闭包和闭包相关的知识点:
闭包介绍
闭包的概念
引用MDN官方给出的解释:
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。
换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
闭包使得函数内部的变量在函数执行完毕后依然存在,是一种编程技巧
闭包的例示
function outerFn(){
const a = 1;
function innerFn(){
//这里形成了闭包,使内存函数innerFn()可以访问到外层函数outerFn()的变量a。
console.log(a);
}
return innerFn; //返回innerFn函数
}
//让func接收outerFn函数调用。
//其实就是func接收到outerFn返回的方法innerFn
const func = outerFn();
//这里实际上就是在调用innerFn方法。
func(); //输出1
像在上面的例示中,我们使用闭包来返回outerFn函数的内部变量a。
为什么要使用闭包
闭包可以实现变量的私有化、局部化。因为JavaScript不像Java那样有private来声明私有变量,所以我们可以使用闭包来实现变量私有化。
如,上面的for循环问题,我们可以使用闭包来解决,使全局变量 i 局部化。
又或者下面的问题:我们写一个累加计数的函数
//要累加的变量
let num = 0;
//累加方法
function addCounter(){
num++;
console.log(num);
}
//输出结果
addCounter();
这里如果我们在控制台如下操作:
那么num就会被修改为20。num的累加计数就出错了,会有被修改的风险。
那么如果我们如果把let放入到函数内部呢?
//上面的累加计数把变量放到函数内部:
//累加方法
function addCounter(){
//要累加的变量
let num = 0;
num++;
console.log(num);
}
//输出结果
addCounter();
在控制台重复上面的操作,可以看到我们即使修改了num的值,再调用累加方法还是会输出1,这样num就不会被随便修改了。
但这样做反而有一个问题:
就是 num 被声明在 addCounter 函数内部,如果该函数被重复调用,我们的每次调用都是在重新声明一个 num ,并重新初始化为0。这样的话num的值就不会被保存,就无法实现累加的效果。
所以,这时我们就可以用闭包来解决这个问题。
闭包是通过在外部函数中定义一个变量(防止被修改),在内部函数中进行访问修改该变量的值(实现累加效果),从而实现变量的持久化。
//闭包实现累加计数
//累加方法
function addCounter(){
//要累加的变量
let num = 0;
//内部函数访问修改累加变量
return function innerAdd(){
num++;
console.log(num);
}
}
//定义一个变量接收addCounter返回的内部函数,
const counter = addCounter();
//来直接使用内部函数访问修改内部参数,实现累加效果。
//同时因为num是在addCounter中定义的,所以每次调用内部函数时,num的值不会被重新初始化,实现了持久化
counter(); //输出1
counter(); //输出2
counter(); //输出3
三种方式使用闭包
-
把函数作为返回值返回来使用闭包
function outerFn(){ const a = 0; return function innerFn(){ console.log(a); }; } const result = outerFn(); result();
-
把函数当做参数来使用闭包
function outerFn(callback){ const a = 0; callback(); } outerFn(function innerFn(){ console.log(a); });
-
利用匿名函数来使用闭包
(function(a){ console.log(a); })(0);
闭包的经典应用场景
-
计数器
详见上面为什么要使用闭包举的例子。
-
缓存函数
缓存函数结果,方便下次使用直接调用
-
创建私有变量
-
实现节流和防抖
-
etc…
闭包存在的问题
闭包虽然实现了变量的私有化、局部化,但它也存在一个问题,即内存泄漏问题。
闭包使得私有化的变量不会被垃圾回收器给回收,且如果我们大量地循环调用闭包就可能导致内存中产生过多的变量无法被回收,造成资源浪费。
闭包方案解决上文遇到的问题
最后,我们回到上文for循环遇到的问题,来看看闭包是怎么解决的:
for (var i = 0; i < btn.length; i++) {
//匿名函数(立即函数)使用闭包方式
//在匿名函数中我们使用 index 变量而不是使用 i 变量,因为 index 变量是被捕获在闭包内部的,而 i 变量是在循环中声明的,因此其作用域会随着循环的进行而不断改变。
(function(index) {
btn[index].addEventListener("click", function() {
console.log(index);
});
})(i);
}
这里这样使用闭包,既可以解决 i 的值的问题,也可以防止闭包的内存泄漏风险:因为当事件监听函数执行完毕后,对应的闭包也会被销毁。这样闭包实例的引用计数就为零,垃圾回收器就会自动回收它。
简单讲下,变量提升:是把变量的作用域进行提升,提升到所在函数的顶部或全局作用域。这里i被提升为全局作用域。 ↩︎