JS闭包 - 先说基础
我最初对闭包的定义
关于闭包,很多地方都有所谓的标准定义;但我相信很多人和我一样,看了标准定义之后就进入了蒙B状态。下面是我给出的定义;
闭:封闭
包:作用域
闭包:封闭的作用域
我知道看完我的定义你一样进入到了蒙B状态,但是没关系,且听我慢慢道来。
你一定想问我,是不是只要有一个封闭的作为域就能形成闭包呢?我很想说Yes,但是我不能。因为是不是闭包,还需要看它能否表现出闭包的特性。
关于闭包的特性,在基础里是说不完的,稍候我会列举几个闭包基本的特性;关于闭包特性的全面分析,我会再出专门的文章来讲述我的理解。
说闭包,就必须说清楚它所处的语言环境,在不同的语言环境下,闭包的特性是不完全一样的。所以我定的标题是JS闭包,在javascript语言环境下的闭包。
看了对闭包的标准定义后我的疑问
现在,不得不引用一些标准定义,来简化我的表达:
MDN官方解释:
Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, these functions 'remember' the environment in which they were created.
我的翻译:
Closures 就是一个关联了“独立(自由)变量”(这些变量在封闭的作用域内定义,并且只能在这个封闭的作用域内使用)的函数;也就是说,作为闭包的函数记住了创建他们的上下文环境。
关于这个定义是否准确,在此我先不发表意见,但对于以上定义,我有以下疑问:
- 什么叫独立变量或自由变量?
- 怎么记住的?
- 变量只能在封闭的作用域内使用,这个封闭的作用域与闭包是什么关系?
并且我相信很多人与我一样,有这些疑问。接下来我就一一解释这些疑问,当所有的疑问被消除后,你应该就能对闭包有一个基本的认知了。
上面的英文在括号中有对“独立(自由)变量”进行解释说明,但这句话非常具有误导性,想要真正理解闭包,你必须忘了它。
通过其他版本的闭包定义消除第一层疑问
好,我们再来看看下而来自于“必应网典”对闭包的定义:
闭包是指可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。
通过上面这段来自于必应网典的定义,我们可以把上面的第一个疑问消除了。下面给出我的理解:
独立(自由)变量:
- 首先是未绑定到特定的对象,也就是无法使用
obj.attr
的方式访问到的变量。(obj
是一个对象,而attr
是独立变量的变量名) - 不是在这个代码块内或者全局上下文件中定义的,而是在定义代码块的环境中定义的局部变量。
如果你对“什么是绑定到特定对象”不了解,你可以去看一下JS中是如何支持面向对象的,相信你看完后会明白“什么是绑定到特定的对象”。
我相信有人会问,上面 2
中提到的“代码块”是什么?其实我最初读“必应网典”上的这段文字时也有同样的疑问。
请再读一遍必应网典上的定义闭包的那段话吧,第一句:
闭包是指可以包含自由(未绑定到特定对象)变量的“代码块”;
然后继续往下读……,是不是明白了,这段话中的“代码块”其实就是指闭包。那我们把“代码块”三个字换成“闭包”然后再来读一下 2
里面的话:
不是在这个“闭包”内或者全局上下文件中定义的,而是在定义“闭包”的环境中定义的局部变量。
什么是“局部变量”我就不再解释了,如果你不懂,那么请放弃编程;经过以上分析我们对“独立(自由)变量”的含意已经非常清楚了。下面再重写一遍,加深印象。
独立(自由)变量:
- 首先是未绑定到特定的对象
- 不是在这个“闭包”内或者全局上下文件中定义的,而是在定义“闭包”的环境中定义的局部变量。
通过分析推理消除第二层疑问
好了,疑问一已经消除,那就向疑问二进军吧。先看一段闭包的代码:
function ClosuresOuter(){
var random = Math.random();
function ClosuresInner(){
return random
}
return ClosuresInner;
}
Closures = ClosuresOuter();
请仔细思考后告诉我,上面这段代码中,谁是闭包?
我知道有人说 ClosuresOuter 是闭包,也有人说 ClosuresInner 是闭包;这样理解也不能说是错,但真正意义上的闭包应该是上述代码中的 Closures ,通过调用 ClosuresOuter 返回的函数。
有争议,没关系,我们先不讨论谁才是真正意义上的闭包,我们回到最初的目的,解决疑问二:闭包是如何记住独立(自由)变量的?
不过要先确认一点,所有人都认同以上代码中是创建了闭包的。如果你觉得上述代码没有创建闭包,那么请回避。
在函数内可以访问函数外声明的变量,看上面的代码,ClosuresInner 内部使用了在 ClosuresInner 外部定义的变量 random ,所以如果ClosuresInner 是闭包的话,它就是这样记住“独立(自由)变量”的。
在javascript中,上面代码中的ClosuresInner 不仅记住了 random, 还记住了运行函数 ClosuresOuter 所创建的整个环境,这是javascript的语言特性,也是javascript支持闭包特性的前提条件。
现在疑问二告破;
究竟谁是闭包?
在上一节的代码中,random 是否满足“独立(自由)变量”的条件呢?
- 首先这个变量没有绑定到任何对象
- 然后这个变量不能在闭包内或全局作用域内定义,需要在定义闭包的作用域内定义。
好吧,ClosuresOuter 已经不可能是闭包了。有些认为ClosuresInner是闭包的同学们是不是开始洋洋得意,沾沾自喜了?不要这样,请往下看。
实验截图请仔细看上面的“实验截图”,ClosureOuter 就是上面代码中定义的那个。每次运行 ClosureOuter 都会产生一个新的 random ,请问是谁记住了random?答案是 ClosureInner,但也不是。原因是,每次运行ClosureOuter 同样会产生一个新的 ClosureInner。
这样说吧,ClosureInner其实并不存在,它只是ClosureOuter 作用域内的一个局部变量(函数);如果这个ClosureInner没有被 ClosureOuter 返回并被外层接收返回值的变量接收的话,这个ClosureOuter 运行所创建的临时作用域和作用域内的变量(包含 random 和ClosureInner)会很快被垃圾回收器收回。
所以结论是 ClosureOuter 返回的函数才是闭包(也就是上述代码中的 Closure)。
闭包的基本特性
这个小节只是为了履行前方的承诺,所以不打算用心写,见谅!
- 记住闭包所在的环境;创建闭包的外层函数运行时所创建的环境不会被垃圾回收,只有这样才能让闭包记住它所在的环境以及该环境内的独立(自由)变量;所以使用闭包有得有失啊。
自圆其说
最开始我对闭包的定义是:封闭的作用域。这明显很不靠谱啊。所以现在细化如下:
闭:封闭
包:作用域,环境
闭包:能够记忆被创建时环境的封闭作用域
好了,也是址淡的定义,初学者看后也只能蒙了一B。
我为什么不说闭包是“函数”,而是说成“作用域”
简单了说,是因为我知道java的“内部类”也是一种闭包。
装B一点的说法是:只要能够表现出闭包的特性,就可以称之为闭包。在javascript中只有函数有自己的作用域,所以也只有函数有条件成为闭包。但在其他语言中有很多拥有自己作用域的概念,如:包,类,一个代码块都可以有自己的作用域。所以把说成是拥有闭包特性的函数也只在javascript语言中成立。
总结
之所以有这个小节,是我的一种习惯。
闭包基础就写到这儿吧。有不同的见解的,欢迎吐槽。
为什么不解释疑问三
我都说了这句话:“这些变量在封闭的作用域内定义,并且只能在这个封闭的作用域内使用”,非常具有误导性,想要真正理解闭包,你必须忘了它。你为什么不听呢?哼!