闭包
本文从作用域和预解析出发,之后再去了解闭包(了解这个可以更好的了解闭包的环境)然后后面就再谈谈下闭包的this指向问题以及和闭包密切相关的变量生命周期啦。
推荐大家看看B站闭包,感觉挺ok的
作用域
在了解闭包之前,我们得首先了要解什么是作用域以及预解析的相关内容
作用域:变量可以起作用的范围
全局变量和局部变量
全局变量
在任何地方都可以访问到的变量就是全局变量,对应全局作用域
局部变量
只在固定的代码片段内可访问到的变量,最常见的例如函数内部。对应局部作用域(函数作用域)
没有声明的变量是全局变量。
变量退出作用域之后会销毁,全局变量关闭网页或浏览器才会销毁
块级作用域
任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有let或const变量在代码块外都是不可见的,我们称之为块级作用域。
1 | let b = 1 |
for循环作用域
若在for循环中用let定义i,则每次循环都会产生一个块级作用域
1 | for (let i = 10; i >= 0; i--) { |
若使用var定义,则为全局作用域
作用域链
只有函数可以制造作用域结构, 那么只要是代码,就至少有一个作用域, 即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。
将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。
1 | function f1() { |
1 | function f1() { |
预解析
JavaScript代码的执行是由浏览器中的JavaScript解析器来执行的。JavaScript解析器执行JavaScript代码的时候,分为两个过程:预解析过程和代码执行过程
预解析过程:
- 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。
- 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
- 先提升var,再提升function。
JavaScript的执行过程
1 | // 案例1 |
这一部分源于很久之前自己学的JavaScript,那个老师讲的很好,就是不知道出处在哪里了。
变量提升
变量提升
定义变量的时候,变量的声明会被提升到作用域的最上面,变量的赋值不会提升。
函数提升
JavaScript解析器首先会把当前作用域的函数声明提前到整个作用域的最前面
1 | f1(); |
闭包
简介
子函数和其访问其他函数的变量统称为一个闭包,如下图
知乎中的解释:「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。
闭包的作用
作用:闭包常常用来「间接访问一个变量」。换句话说,「隐藏一个变量」。
让变量的值始终保持在内存中
如下面定义了一个数组对象,我么们要挑选价格在1-10之间的商品,于是我们定义了一个函数,a和b是这个函数的变量,我们在里面又定义一个函数去获取这两个变量并求我们需要的值,最终在全局去使用这个值,于是我们就用到了闭包。
这样写的优点:防止变量污染全局(函数作用域),且可以得到函数体内的私有成员(return)
1 | let commodity = [ |
注意:用到return是因为我们需要到这个函数体内的值,不能说他就是闭包的一部分。
闭包中this的指向问题
下面有这么一段代码
1 | let fruit = { |
我们来输出这个结果
1 | let a = fruit.say() |
果不其然,和我们预想的一样,它输出的结果就是apple,因为this指向的是调用该方法的对象,即fruit,所以自然而然地就输出name属性了
然后,让我们来看看下面这个场景,当我们使用了闭包呢,又会发生什么样的现象
1 | let fruit = { |
是的,没错,输出结果就跟注释一样,是undefined,为什么呢,仔细看看,你会发现,首先我们调用了fruit中的say()方法,然后用a去接收,接着在我们打印a()时,这个时候函数体内的this指向的其实就是window对象(也就是说,我们在全局window对象中调用了a()这个方法),于是乎,它找不到window中的name属性,也就打印出了undefined,这个就是在使用闭包时可能会遇到的问题,也是在箭头函数未出现前会出现的问题
解决方法有两种,如果前面有仔细阅读的话,你就会发现,其中一种就是用箭头函数去解决这个问题,下面放出两段解决方法
方法一
我们只需要修改两行代码,一个就是接收对象,另一个则为调用这个对象
1
2
3
4
5
6
7
8
9let fruit = {
name: 'apple',
say: function() {
var This = this //定义变量接收该对象
return function() {
return This.name //使用
}
}
}方法二(提倡)
使用箭头函数替代
1
2
3
4
5
6
7
8let fruit = {
name: 'apple',
say: function() {
return () => {
return this.name
}
}
}变量的生命周期
对于全局变量来说,它们的生命周期是永久的,除非我们主动去销毁它
对于函数作用域下的变量来说,当函数执行完退出后,这些变量就失去了价值,也会随着函数的退出而消失
而在闭包里,同样都是函数,只不过是以嵌套的形式呈现,这个变量就不会被销毁呢
回到前面this的指向问题中的代码例子中去,当我们在执行var a = fruit.say()时,首先它会在执行say()时返回一个匿名函数的引用,它可以访问到a()被调用时产生的环境,局部变量就会一直存在在这个环境里(这里每定义变量可能不是很明显,下面会有另一个例子),而当我们需要频繁调用a()时,这个变量就会一直被用到,那么它就没有被销毁的理由,所以就会一直存在了
1 | //普通函数 |
1 | //闭包 |
闭包的更多作用*
可以看看《JavaScript设计模式与开发实践》这本书有关闭包的相关内容
- 封装变量
- 闭包和面向对象设计
- 用闭包实现命令模式
- 内存管理