JavaScript闭包及相关

这是在前端领域的第一篇文章,按照常理我应该写点html和css的相关内容,但是我想放在后面来写,第一篇文章还是说一说曾经觉得很魔幻的JavaScript,就从js中最神秘的闭包开始说起吧。

什么是闭包

对于初接触js的人,闭包是一个很难懂的概念,甚至是有很长时间开发经验的人,往往也不一定能说的清楚透彻,首先来看网上随便就能搜索到的js中闭包的定义:

所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

从这种书面化的定义中很难获取到直接明了的有效信息,所以这个概念先放在这里,要想了解闭包,我觉得可以先从js的作用域链说起。

作用域链

不考虑es6和with语句,我们大体上可以说js语言是不存在传统的块作用域的,但是存在函数作用域。所谓函数作用域就是指创建一个函数时候,函数的内部变量是只存在于函数内的,此时这个函数就形成了一个函数作用域。

我们都知道,不论是在浏览器还是node或其他js运行环境下,js所有代码都运行在一个全局作用域里面,而上面分析了函数可以创建作用域,如果在函数中再次声明函数,就会在里面再次形成作用域,考察下面一段程序

1
2
3
4
5
6
7
var a = 3;
function f1() {
var a = 4;
function f2() {
var a = 5;
}
}

此时就存在了三个作用域,如图所示

这三层作用域是嵌套关系,里面的可以访问外面的,外面的无法访问里面的。此时,如果内部作用域想要访问外部作用域中的元素,救需要一层层向外找,考察下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 3;
var b = 11;
var c = 25;
function f1() {
var a = 4;
var b = 13;
function f2() {
var a = 5;
console.log(a);
console.log(b);
console.log(c);
}
}

在这种情况下,内层函数在寻找变量时候情况就有所不同了,直接看图

从图中可以很清晰地看出,在寻找变量的时候是按照由内到外,按照层级来寻找的,这就形成了一个链条,可以称之为作用域链。

词法作用域

理解了作用域链,应该就可以理解js的词法作用域了。与词法作用域相对的概念是动态作用域。先来看下面一段代码,它的输出结果是什么

1
2
3
4
5
6
7
8
9
function f1() {
console.log(a);
}
function f2() {
var a = 5;
f1();
}
var a = 4;
f2();

正确的输出结果是4,有疑虑可以看下面的一组定义

词法作用域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,去函数定义时的环境中查询。

动态域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,到函数调用时的环境中查。

这就是这个问题的全部,js中采用的是词法作用域,所以变量要在函数定义时候的环境中去找,如果没有,那就沿着作用域链向上查询。

理解闭包

有了上面的知识铺垫,其实就不需要纠结闭包的概念了,因为闭包的本质就是上面的变量解析过程,在实际应用中,我们使用闭包的主要用途主要有两方面,下面来看一下具体的实现来理解一下闭包。

封装变量

看一下下面的例子

1
2
3
4
5
6
7
8
function f1() {
var a = 4;
return function f2() {
console.log(a);
}
}
f = f1();
f();

这段程序中,外界环境是无法直接读取a的,因为a处于函数作用域。但是我们通过创建一个闭包f2,就可以实现访问a的目的,其实本质就是应用了f2会去寻找声明时环境的特点,从而打开了读取函数内部变量的外部接口,采用这种方式可以实现封装,很多模块化编程解决方案都是通过这种方式来实现的。

保存运行状态

考虑下面的代码,它的输出结果是什么

1
2
3
4
5
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}

如果是同步的程序,显然是0 1 2,然而很不幸,这段程序中插入了一个定时器,它是异步执行的,带来的影响就是它的的输出结果是3 3 3。关于异步编程的更多内容我想我以后会补充,在此只简单解释一下为什么会这样。js是单线程模型,所以同一时刻只能执行一件任务,而异步操作会被丢到一个队列中,等到主线程执行完毕,再去队列中依次取出并执行。所以,当执行打印操作时候,其实主线程早已执行完毕,i=3,所以只能输出3。

那么如何解决这一问题呢,想想我们的需求,我们是想要在每次执行时候打印对应时刻的i值,每次都不一样,需要把每个状态下的i保存下来。想想闭包的概念,是不是很清楚了?没错,我们只要创建一个闭包,就可以实现需求,代码如下

1
2
3
4
5
6
7
for (var i = 0; i < 3; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000);
})(i)
}

现在回来看闭包的概念,是不是清晰多了,理解了本质,闭包实际上并不难,不过也有一些需要注意的地方。

注意事项

使用闭包,一个最需要注意的问题就是,闭包会产生大量无法释放的内存,不能滥用,在实际应用中,要在需要的时候使用闭包,不要刻意使用闭包。

回调函数

关于回调函数和异步编程的相关内容,后面我应该会有文章详细分析,这里只想说一些和本文相关的内容,回调函数其实也是闭包。我们将一个回调函数作为变量传递给另一个函数时,这个回调函数在包含它的函数内的某一点执行,就好像这个回调函数是在包含它的函数中定义的一样。所以回调函数可以拿到执行的某个点,某个状态的信息。

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!