“闭包”这是js语言中最强大且最有特色的技术点之一了。我敢肯定但凡写过js的前端开发人员都听说过“闭包”这个词,毕竟它的名声在外,如果你都没听过“闭包”的话,都不敢称自己是一名JS的开发人员了,但却不是每个人都能很清楚的理解并能熟练自如的运用这一强大的技能,今天就来谈一谈我对“闭包”的理解。
我们都知道在javascript中变量的查找是遵循词法作用域(下文会详细说明这个概念)规则的。当然我们这里不考虑widh和eval()这两种可以在运行时修改或者创建新的作用域来“欺骗”词法作用域的情况。正常情况下一个函数内部是可以通过原型链向上级作用域查找它所需要的变量,但是反之,上层作用域是不能访问到一个函数内部的变量的。如下示例辅以说明:
code:
<script>
var foo = function () {
var innerValue = '我是在foo 函数内部定义的变量';
}
console.log(innerValue)
</script>
编辑
可以在控制台中看到运行效果如上,我们在foo函数外部是无法访问foo函数内部所定义的变量的。所以浏览器会抛出一个ReferenceError的错误。表示在当前的词法作用域中并没有找到innerValue这个变量。
但是有些时候需要能够在这种情况下访问的到innerValue这个变量,那有没有一种技术可是实现这个需求呢?答案是肯定的了,所运用的技术就是我们今天的主角“闭包”了。
一、词法作用域
函数查找都遵循词法作用域规则,那我们先看一下什么是词法作用域呢,好像比我们经常提到的作用域前面加了个“词法”前缀。我们看下其定义:
词法作用域:词法作用域就是在定义词法阶段的作用域,换言之,词法作用域就是由你在写代码时将变量和块作用域写在哪儿决定的,因此当词法分析器处理代码时会保持作用域不变(忽略with,eval())。
相对“词法作用域”还有一个概念是“动态作用域”有一些其他的编程语言使用的规则比如Bash, Perl中的一些模式。(这儿提到动态作用域纯粹是为了与js中的词法作用域相做比较,与本文探讨的“闭包”无关)
二、如何产生闭包
通过词法作用域的定义我们可以发现,访问一个函数的时候,就可以持有定义该函数所在的词法域,这是一个很有用的特性,正是利用了词法作用域的这个特点,才会有闭包这么强大的功能。那问题就好像有了解决的方法了,我们为了访问到变量innerValue变量,首先要持有这个变量所在的作用域,那如果foo函数返回的是一个函数内部定义的另外一个函数的话,那我们就可以持有想要的作用域了。看如下代码:
<script>
var foo = function () {
var innerValue = '我是在foo 函数内部定义的变量';
var innerFn = function () {
console.log(innerValue)
}
return innerFn
}
var fn = foo();
console.log(fn()) //hey,这就是"闭包"的效果。
</script>
编辑
瞧,这下我们在foo函数外边访问到了innerValue这个变量。这就是“闭包”的威力。
原本按照正常执行的情况下,foo函数运行完之后,在引擎的垃圾回收机制的作用下通常foo()内部整个作用域都会被销毁,而“闭包”就会阻止这件事情发生。因为foo函数返回了一个innerFn的函数,所以事实上内部作用域依然在使用,并没有被回收。而此时恰巧innerFn函数所在的位置使得我们持有foo()内部作用域的闭包,使得该作用域一直存活,以供innerFn函数在后面的任何时间进行引用。innerFn()会依然持有对该作用域的引用,这个引用就被称之为“闭包”。
之后在我们运行fn()这个函数的额时候,根据词法作用域规则,innerFn函数会首先在自己内部查找innerValue这个变量,并没有找到,然后会沿着原型链像上查找即查找foo函数有没有innerValue这个变量,然后发现找到了,接着就是欢天喜地的吧这个变量给打印出来,到这里也就大功告成了。达到了我们想要的目的。
下面我们用一句话来对“闭包”进行说明下:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外进行的。
更多的时候,闭包的直观效果是在后面一句,即函数即使是在当前的词法作用域之外进行的。
三、内部函数传递的形式
上面的例子我们是将函数当做返回值来进行传递的,事实上,无论用何种方式对函数类型的值进行传递,都是可以产生“闭包”的。
比如下面举个例子我们将会将函数当做参数来传递。
<script>
function foo1() {
var str = '我是在foo1函数中定义的变量';
function innerFn() {
console.log(str)
}
foo2(innerFn)
}
function foo2(fn) {
fn()
}
foo1()
</script>
编辑
上面的例子就是直接把函数当做参数来传递也是完全没问题的,产生了闭包。
再比如直接把函数赋值给全局变量也是没问题的:
<script>
var fn;
function foo() {
var str = "我是在foo函数内部定义的"
function innerFn() {
console.log(str)
}
fn = innerFn; //将函数innerFn赋值给全局变量fn
}
foo();
fn();
</script>
编辑
可以看出也是没有任何问题的,所以无论通过何种手段将内部函数传递到所造的词法作用域之外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会产生闭包。
四、回调函数中的闭包
相信通过上述一些列的列子使得大家对闭包应该有一个比较深刻的认识了,其实在我们日常写的代码之中已近存在了很多的“闭包”的使用,只是我们当时在写的时候并没有意识到罢了。举个常见的定时器例子。
code:
<script>
var foo = function (msg) {
setTimeout(() => console.log(msg),1000)
}
foo('我是一个msg')
</script>
这段代码会如期在1s之后在控制台上输出’我是一个msg’的字符串,在我们的foo函数内部有一个定时器,这个定时器中的函数会在1秒钟之后运行,因为在setTimeout的回调函数中有着对msg变量的引用,所以在foo函数执行后foo内部作用域并没有立即销毁,而是保留在内存之中等待1s之后留给箭头函数引用。
所以在我们日常开发中,无论你是使用定时器,时间监听器,Ajax请求,跨窗口通信,web worker或者其他的任何异步(或者同步)任务中,只要使用了回调函数,实际上就会产生闭包。因为回调函数会持有完整的他在定义时的词法作用域。
五、闭包的优缺点
优点:
- 提供了一种强大的“手段”使我们可以跨作用域访问变量,避免额外的写很多逻辑。
- 实现了一种封装的手段,避免了命名冲突。
缺点
- 缺点也是很明显的,就是占用内存,如果滥用闭包的话,会出现内存泄漏。
- 性能问题,每次使用闭包的时候都会出现夸作用域访问,每次的跨作用于访问都是需要消耗性能的。
以上就是我对闭包的理解,希望能够帮助大家加深对闭包的理解。