JavaScript中的闭包

其实这个专题之前就写过一些东西,不过当时属实是个小老弟,非常生涩而且里边的东西都是复制粘贴来的。想表达的东西很多但是都没有表达清楚。所以面临面试的我打算重新总结一番,当作一个回顾吧

是什么

什么是闭包?

MDN:包是函数和生命该函数的词法环境的组合

Tyler McGinnis:子函数在其父级变量环境上“关闭”的概念

w3cschool:词法表示包括不被计算的变量的函数。也就是说,函数可以使用函数之外定义的变量

阮一峰:能读取其他函数内部变量的函数

简书某博主:一个函数访问了他的外部变量

《你所不知道的JavaScript》:当函数可以记住访问所在的词法作用域时,就产生了闭包——即使函数是在当前词法作用域之外执行

从我个人理解上来看《你所不知道的JavaScript》一书给出的定义最适合我们理解。因此我们着重以其思路进行判断

实现

1
2
3
4
5
6
7
8
9
10
11
function foo(){
var a = 2
function bar(){
console.log(a)
}

return bar
}
var baz = foo()

baz() // 2

如图,foo执行后将bar作为返回赋给了baz,使得bar得以在自己定义的语法作用域之外的地方执行。

这并不是最神奇的地方。我们之前提到了js的垃圾回收机制,当调用结束后会自动回收内存。但是闭包会阻止这一过程的进行。此例中按理说foo不会再使用,但是给baz赋值时,引擎通过RHS查找的会查找foo的地址,此过程就阻止了foo的回收。归根到底还是由于bar本身的调用使得foo的内部作用域仍在被引用,此引用就叫闭包

函数在别处被调用时也是闭包——跳出本来的作用域到其他作用域完成其功能

加上循环再试试

《JavaScript高级程序设计》书中提到了一个很有趣的例子:

1
2
3
4
5
6
for(var i=0; i<10; i++){
setTimeout(function timer(){
console.log( i )
}, i*1000)
}
// 10s内每秒打印一个10

我们上边讲到了作用域的向上查找方式,以及块作用域的定义

延迟函数的回调会在循环结束时才执行。Why?因为其缺陷在于我们试图假设循环中的每个迭代都在运行时都会自己捕获一个i的副本。但事实上尽管循环中的10个函数都是在各个迭代中分别定义的,可其共享一个作用域,而此作用域中只有一个i

到底怎么办

由上述可知我们需要更多的闭包作用域!尤其是每个迭代都需要一个闭包作用域,如此才可以实现i的迭代

1
2
3
4
5
6
7
8
for(var i=0; i<10; i++){
(function(){
setTimeout(function timer(){
console.log( i )
}, i*1000)
})(i)
}
// 正确打印
第一个咋错了?

在这里插入图片描述
首先是错误输出案例。

我们从上一篇博客中知道函数作用域包括参数变量以及方法。所以我们将同一个作用域中的这些部分包含在一个圈内(为了省事我就不画全局作用域啦)

①中包含了传入参数i(每次循环都会被迭代,所以下方i*1000中的i也会同步更新。)和函数setTimeout

②作用域中则是timer内部的i。由于此作用域中未定义此变量,所以引擎会向上级作用域讨要这个i的值。而闭包的特点前面我们讲过(尽管循环中的10个函数都是在各个迭代中分别定义的,可其共享一个作用域,而此作用域中只有一个i),所以只能得到循环结束后的结果10作为i的值。

那这个为啥又对了?

在这里插入图片描述
我们前面说到IIFE(Immediately Invoked Function Expression,立即执行函数表达式)模式会创建全新的作用域。所以在两层作用域之间嵌套一层,就可以实现参数的传递啦。

第①层依旧平平无奇,只有传入的迭代变量i以及那个IIFE

第②层就有猫腻啦!我们将上一层的迭代变量作为参数传入了这个立即执行函数表达式,如此即可保证变量的同步更新

第③层则是遵循之前我们所讲,向上一层作用域寻求变量i。由于上一层(②层)的i是作为参数传入(可以理解为实时更新),所以自然可以取到正确的值咯~

简化
setTimeout的第三个参数
1
2
3
4
5
for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
let
1
2
3
4
5
6
for(let i=0; i<10; i++){
setTimeout(function timer(){
console.log( i )
}, i*1000)
}
// 正确打印

是的没错!我们可以使用let偷个懒!上一篇关于作用域的博客我们已经讲过了,let会创建一个块作用域,这个块作用域就像是传统面向对象中的一样,没有这么多勾心斗角,只有无尽的岁月安好。所以说版本的进步真的是我等的一大福音啊。

let中的块级作用域相当于下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}

优缺点

说了这么多咱们最后总结一下使用闭包的优缺点

优点

  • 可以读取函数内部变量
  • 可以让这些变量始终保存在内存中(闭包默认会保持其所引用的环境变量)

缺点

  • 闭包会默认保持其所引用的环境变量。这可能导致内存泄露
    • 解决方法:推出前务必将不再需要的变量值设为空,方便垃圾回收机制回收
  • 可以在函数外部改变函数内部变量的值,造成安全隐患。

好了今天的话题我们就讲到这里
告辞