前言
相信很多小伙伴在工作或者面试过程中都遇到过这个问题,作为经典的前端面试题之一,它高频地出现在我们的求职生涯中。所以,了解和掌握它也就变得十分必要了
读完这篇文章,你或许就会知道:
- 闭包是什么,它是怎么形成的
- 为什么要使用闭包
- 闭包会造成哪些问题
如果文章中有出现纰漏、错误之处,还请看到的小伙伴多多指教,先行谢过
以下↓
作用域
what? 不是在说闭包么,怎么又扯到作用域上面去了
稍安勿躁,在我们了解闭包之前,还是很有必要先了解一下 JavaScript
中的作用域
我们都知道在 JavaScript
中存在着全局变量和局部变量,全局变量可以在任何地方访问到,然而局部变量只能在当前作用域中访问。全局作用域是不能直接访问局部作用域中的变量,而局部作用域可以直接访问全局作用域当中的变量
就像一个代码块儿或函数被嵌套在另一个代码块儿或函数中一样,作用域也会被嵌套在其他的作用域中。所以,如果在直接作用域中找不到一个变量的话,就会咨询下一个外层作用域,如此继续直到找到这个变量或者到达最外层作用域(也就是全局作用域)
说了这么多,其实说白了,所谓 作用域就是一组规则,它决定了一个变量(标识符)在哪里和如何被查找
试想一下,现在有一个这样的需求:我们想在全局作用域拿到局部作用域的某一个变量该怎么去做呢?
初识闭包
在
JavaScript
中闭包无所不在,你只是必须认出它并接纳它
正常情况下,我们并不能拿到局部作用域的变量。但是,我们可以使用变通的方式:定义一个函数
让我们看一下这段代码
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
}
这样在函数 foo
中定义一个 bar
函数,在这个函数中我们就能访问到定义在函数 foo
中的变量 a
。既然我们这样就可以访问到 foo
函数里面的变量,那么,只要我们将 bar
这个函数作为返回值输出,不就实现我们的需求了么? 是的!
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
return bar
}
var result = foo();
result() // 2
这就是闭包
让我们来看看这个函数做了什么:
- 创建了一个函数
foo
- 函数里面创建了一个变量
a
与函数bar
- 返回函数
bar
现在,我们就对闭包有了一个基本的概念:定义在一个函数内部的函数
再遇闭包
词法作用域:简单理解为作用域是由编写时函数被声明的位置定义的
还是来看一下下面的代码
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn();
}
与前面示例不同的是,这里我们并没有将函数 baz
返回,而是将它当做值传递给 bar
这个函数,然后在 bar
这个函数里面执行,函数 bar
保持了函数 baz
的引用
相同的是,实际上函数 baz
都在它被编写时的词法作用域之外被调用,bar()
依然拥有对那个作用域的引用,而这个引用称为闭包
这就是闭包
好了,看到这里是不是还有点懵呢,让我们再来看一个更加常见的示例
for(var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
毫无疑问,运行上面的代码会输出 5
个 6
.很明显,我们得到的结果是 i
在循环之后的最终值
那么,为什么会是这样呢?
其实,由于作用域的工作方式,我们在定时器函数中访问到的 i
是共享到全局作用域的上的,它只有一个,就是最终循环结束的值
想让这个循环显示我们想要的结果也很简单,只需要这样:
for(var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j)
}, 1000)
})(i)
}
使用一个立即执行函数将定时器函数包裹起来,在这个函数中定义一个变量 j
,然后将 i
当做值传递进去。这样,在每次迭代的时候,变量 j
都会拥有 i
的一个拷贝,自然得到了我们想要的结果
同样的,这也是一个闭包
当然,我们也可以使用ES6
中的let
关键字声明变量i
通过前面的一些示例,我们不难发现:闭包其实并没有特定的格式,只要满足一些条件,它就是闭包
所以:
闭包就是当一个函数即使是在它的词法作用域之外被调用时,也可以记住并访问它的词法作用域
闭包的用途
闭包最大的用途:
- 读取函数内部的变量
- 让这些变量始终保存在内存中
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result
实际上就是闭包 f2
函数。它一共运行了两次,第一次的值是 999
,第二次的值是 1000
。这证明了,函数 f1
中的局部变量 n
一直保存在内存中,并没有在 f1
调用后被自动清除
- 使用闭包模拟私有方法(数据隐藏和封装)
私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
这个环境中包含两个私有项:名为 privateCounter
的变量和名为 changeBy
的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包
闭包的问题
闭包的用途在一定程度上也造成了很多问题,比如:闭包会使函数中的变量始终保存在内存中,不能被 JavaScript
的垃圾回收清理,很容易造成内存消耗过大,影响程序性能
后记
闭包的合理运用,会让我们在开发中写出更优雅和干净的代码。但是,不合理地滥用闭包,也会造成很多性能问题,从而使项目维护成本增加。
所以,如何合理地使用这个有趣的东西,还需要我们多多钻研和摸索,相信你一定可以对它越来越熟悉
最后,推荐一波前端学习历程,不定期分享一些前端问题和有意思的东西欢迎 star
关注 传送门
参考文档
You-Dont-Know-JS
闭包 - MDN
学习JavaScript闭包 - 阮一峰