前言
今天我们来爬一爬js中的一座大山--闭包,在众多程序员口中闭包似乎并不容易翻越,但在这里看完这篇文章,定会让你对闭包有一个更深的理解。
正文
在了解闭包概念之前,我们还需提前了解两个概念,一个是调用栈,另一个就是作用域链了,如果这两个概念你都会了,那大佬们可以直接跳过这两部分直接看闭包那块了
1.调用栈(call stack)
首先,什么是调用栈呢,正经一点来说调用栈其实就是用来管理函数调用关系的一种数据结构,是计算机内存中的一个数据结构,用于跟踪程序的函数调用和返回过程。它是一个栈(先进后出)结构,记录了函数的调用顺序和执行流程。
举个做饭的例子来帮助我们理解,相信各位都非常喜欢洋葱吧,那现在我们要做一道美味的爆炒洋葱,最开始我们需要切洋葱,所以我们调用了一个切洋葱的函数,栈是空的,这个函数被添加到了调用栈的顶部,然后在函数内部,我们会执行切洋葱的操作,切洋葱操作结束后这一函数就会弹出调用栈,然后我们需要调用炒洋葱的函数,炒洋葱这一函数就会被放入调用栈中,在执行炒洋葱的过程中我们需要加调味料了,所有我们现在就要调用加入调味料的函数,将这一函数添加到栈顶并执行这一函数,当加入调味料这一函数被执行完毕后,这一函数也会弹出调用栈,控制流程返回到炒洋葱函数,直到这函数执行完毕也弹出调用栈,调用栈就变为空。
举这样一个例子是不是就好理解多了,我们不仅理解了调用栈的概念还学到了如何做一道菜是吧,离未来的老婆又近了一步呢( ͡° ͜ʖ ͡°)
2.作用域链(Scope Chain)
接下来是作用域链了,作用域链就是通过词法作用域来确定某作用域的外层作用域,查找变量由内而外的这种链状关系叫作用域链,当代码在一个作用域中查找变量或函数时,它会先从当前作用域开始查找,如果没有找到,就会沿着作用域链向上查找,直到找到为止。
来个例子加强下理解:
scss
function outer() {
var x = 10;
function inner() {
var y = 20;
console.log(y); // 20
console.log(x); // 10
console.log(outer); // 函数 outer 的定义
console.log(outer()); // undefined,因为 outer 函数没有返回值
console.log(outer()); // undefined,因为 outer 函数没有返回值
}
inner();
}
outer();
在上述示例中,函数 inner
的外部作用域是函数 outer
,全局作用域的外部作用域为 null
。通过作用域链,我们可以在 inner
函数内部访问到外部作用域中的变量 x
,以及外部作用域中的函数 outer
。
3.闭包
好了,前戏搞了这么久,各位应该舒服了吧,接下来正式进入文章的高潮部分--闭包,还是老样子,先谈谈什么是闭包,在js中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量的,当内部函数被返回到外部函数之外时,这时即使外部函数执行结束了但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存中,那我们把这些变量的集合称为闭包。
先给大家看段代码吧
ini
var arr = []
for(var i = 0;i < 10;i++){
arr[i] = function(){
console.log(j);
}
}
for(var j = 0;j < arr.length;j++){
arr[j]()
}
大家看这段代码输出的是多少呢,是不是都以为是'0123456789'呢,其实非也,输出的其实是'十个10';因为在第一个循环中,函数内部的console.log(j)并没有立即执行,而是在第二个循环中执行。在第二个循环中,j已经被循环递增到了10,所以打印出的结果都是10。那有同学就会问了,这和闭包有什么关系呢,诶~,我知道你很急,但你先别急,先忍忍,我们有没有什么办法不该第二个循环的前提下可以让它输出变成'0123456789'呢。因为是讲闭包,所有当然是用闭包,那怎么用呢,仅靠几句话肯定是不会用的,所有接下来我们还是来帮我们理解一下闭包。
先看代码
javascript
function a(){
function b(){
var bbb = 234
console.log(aaa);
}
var aaa = 123
return b
}
var c = a()
c()
如果我们不知道闭包这个概念的话,这段代码输出是不是报错,我们会这样理解:在函数b中,变量aaa是在函数a中声明的,但是在函数b中使用了。由于函数b是函数a的内部函数,它可以访问函数a中的变量。但是在函数a执行完毕后,函数a的作用域会被销毁,所以变量aaa也会被销毁。当我们尝试在函数b中访问变量aaa时,会发生引用错误,因为aaa已经不存在了。但是闭包中有一个非常重要的概念内部函数如果返回到外部函数时,即使外部函数执行结束了但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存中。 所以在这段代码里面,,函数b是在函数a内部定义的,并且在函数a内部访问了变量aaa。即使函数a执行完毕后,函数b仍然可以访问并使用函数a内部的变量aaa。所以当我们执行c()时,函数b会打印出变量aaa的值,即123。这就是闭包的特性,它允许函数在其词法作用域外部继续访问和使用变量。
这样是不是一下子豁然开朗了呢那再让我们想想前面那个问题吧,怎样输出0123456789呢?给你们看下
ini
var arr = []
for(var i = 0; i < 10; i++){
arr[i] = (function(num){
return function(){
console.log(num);
};
})(i);
}
for(var j = 0; j < arr.length; j++){
arr[j]();
}
在这个修改后的代码中,我们使用了立即执行的函数表达式(IIFE)来创建闭包。在每次循环中,我们将变量i作为参数传递给IIFE,并将其命名为num。然后,在IIFE内部,我们返回一个新的函数,该函数打印传递进来的参数num。
由于每次循环都会执行一次IIFE,并将当前的i值作为参数传递进去,所以每个新函数都会捕获并保存其对应的i值。这样,当我们执行arrj时,会调用对应位置的新函数,并打印出其保存的参数num,即当前循环中的i值。
通过这种方式,我们避免了在第一个循环中共享同一个变量j的问题,而是使用闭包来创建了独立的作用域,每个新函数都可以访问并保存其对应的i值。这样,我们就实现了按顺序输出0123456789的效果。
缺点
这样一看,哦哟,怎么有这么好的方法,闭包难道这么好用吗,其实不然,它的缺点也很明显,最简单的就是内存消耗:闭包会导致额外的内存消耗。由于闭包会捕获外部函数的变量和作用域链,这些变量和作用域链会一直存在于内存中,即使外部函数已经执行完毕。如果闭包被滥用或不正确使用,可能会导致内存泄漏或占用过多的内存。