闭包的概念出现于60年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么JavaScript中有闭包: 因为
JavaScript中有大量的设计是来源于Scheme的;
闭包的定义
维基百科的定义
(1)闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures)
(2)是在支持 头等函数 的编程语言中,实现词法绑定的一种技术;
(3)闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
(4)闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在补充时被确定,这样即使脱离了捕捉时的上下文,它也能
照常运行;
MDN
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭
包(closure);也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
coderwhy的总结
一个普通的函数function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包;
从广义的角度来说:JavaScript中的函数都是闭包;
从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用于的变量,那么它是一个闭包;
把所有的定义抛开,我们可以发现,这几种说法中都有对自由变量的字眼,那么我们首先要明白,什么是自由变量。
自由变量是在一个作用域中访问了其他作用域的变量,这个变量就被称为自由变量。
var n = 10
function foo(){
console.log(n)
}
比如在上述的代码中,我们在全局声明了n变量,又在函数foo中访问了n变量,此时,n变量就是一个自由变量。因为全局作用域
是一个作用域,foo函数也形成了一个函数作用域,我们在foo的函数作用域中访问了全局作用域的变量n,此时就产生了跨作用域访问,
访问到目标是n变量,所以n变量就是一个自由变量。
如何理解闭包
在编程界对闭包的定义是有很大的分歧的,会有好几种理解。这里我们不得不说,中国的语言是博大精深的,同样的一句话,可以有好几种解释,甚至是一个词都可以有好几种理解,所以对一个东西的理解有分歧是正常的,我们首先要接受这种分歧,不要去非得争出来个一二三来,没什么意义,我们只需要理解每一种分歧具体的含义,那么我们不就比所有还在那里争执的人对这个概念有更深的理解吗?
第一种理解:javaScript 中的函数都是一个闭包
对于这种理解的人来说,他们认为函数就是一个闭包的原因是这样的,仔细研读上述的几种定义,我们可以这样理解,闭包包含两部分,一个是函数,一个是函数能够引用到的变量,所以闭包可以用如下
结构表示
闭包 = {
函数
函数访问到的自由变量
}
如果这样来解释闭包,那么闭包很简单,闭包 = 函数 + 自由变量,有这两个东西的就算的上是闭包。
那么,下面这几行代码就是一个闭包。
var n = 10
function foo(){
console.log(n)
}
首先foo是一个函数,然后foo中产生了跨作用域访问变量,即有自由变量,所以这几行代码虽然简单,但它就是一个闭包,首先
它有自由变量n,也有一个有自由变量的函数foo。
即使这样解释,我们也应该理解为,是变量n,和函数foo 一起构成了闭包,那为什么说函数就是一个闭包呢?因为这种理解的人
在 “函数能访问到外层作用域的变量和函数有没有访问到外层作用域的变量”钻了语言的漏洞,他们认为我的函数即使没有访问外层作用
域的变量,但我是有这个访问的能力的,所以我满足第二个条件,因为我是个函数,所以我也满足第一个条件,所以我就是闭包。甚至,
n这个变量 你不定义,我也是满足,我虽然不能访问n,我还可以访问全局作用域中的window,这样子我也满足条件,所以我是对的。
你仔细看看,人家说的也没错,但是在这里我门只能说这个理解没错,只是不够严格。
第二种理解:闭包必须被一个函数包裹,且能够访问其外层函数的作用域中的某个自由变量,但外层函数不必被调用。
如何理解这种理解我就不再赘述,既然能理解第一种说法,就必然能接受这种说法,这里只对这种说的不严格的地方解释一下,我
认为这种说法不严格的地方在于,如果外层的函数不被调用,我们知道js引擎对函数的调用是懒加载的,如果不调用,就不会解析,
那么自由变量就没有在内存中存在,更不会常驻,所以说这种说法也不严格。
第三种理解:闭包必须满足如下几个条件
(1)函数嵌套函数
(2)函数内部可以引用函数外部的参数和变量
(3)参数和变量不会被垃圾回收机制回收
(4)外层函数被调用
这种理解是严格意义上的闭包的理解。它完全满足所有闭包的定义,也符合我们在日常开发中使用闭包的应用场景。这里顺便提一
下闭包的优点:
(1)全局变量可以重复使用,但是容易造成变量污染。局部变量仅在局部作用域内有效,不可以重复使用,不会造成变量污染。
闭包结合了全局变量和局部变量的优点。
(2)保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
(3)在内存中维持一个变量,可以做缓存
在本文下面论述中的闭包特指严格意义上的闭包
闭包是什么样子的?
function makeAdder(count) {
return function(num) {
return count+num
}
}
var add10 = makeAdder(10)
console.log(add10(5))
上面的 function(num) {return count+num}就是一个严格意义上的闭包。
(1)首先存在函数嵌套,
(2)被嵌套的函数可以访问其所在的作用域的参数和变量,即
function(num) {
return count+num
}
可以访问makeAdder中的参数count,如果makeAdder 声明了其它变量,这个函数也可以引用。
(3)makeAdder这个外层嵌套函数被调用过,这样它捕获的count变量就会常驻内存。
(4)参数和变量不会被垃圾回收机制回收,这里指count没有被垃圾回收机制回收。
那count 为什么没有被回收,而是常驻内存了呢,这里我们来分析下这几行函数在内存中的形态。
闭包在内存中的形态
正常情况下,当makeAdder函数执行完毕后,AO对象会被释放掉
(根据GC机制,如果一个对象从根部不可达,他就会被释放掉),但是在这个函数中,虽然makeAdder被执行完了,但是我们外部还
有一个add10引用着makeAdder中返回的闭包,这个闭包(function(num) {return count+num} )是一个函数,存在FEC,
FEC中存在作用域属性,其作用域属性是自己的AO和其父作用域的AO,所以这里其父作用域即makeAdder中的AO有被引用到,
所以不会销毁,而count 就是存放在父作用域的AO中的。
闭包的缺点--内存占用
闭包有很多的优点,但同时也存在一定的缺点。
它的缺点就是由于闭包捕获的变量是在内存中常驻的,不会被销毁的,如果这个变量本身占用的内存比较大,而我们对这个变量
的使用是只有一次性的话,就会存在内存泄漏,但是如果我们需要频繁使用这个变量的话,它就不是内存泄漏了,反而是它的另一个
优点的体现,即做缓存的优点。所以我们要辩证的看待闭包的这个缺点。
如果闭包存在内存泄漏的话,如何解决?
只需要将引用到闭包的变量赋值为null即可。注意是所有的引用到闭包的地方。
v8引擎对闭包的优化
这里name 也存在于makeAdder的AO中,但是由于我们在闭包中,没有使用name变量,所以v8 引擎会在函数执行完毕后,将这name 变量释放掉,进行优化,这里需要注意。
但其实,v8引擎的这个优化,我觉得也没错,因为闭包是函数和这个函数引用到的外围环境的集合,从这个角度来看的话,这种优化反而能更好的诠释闭包的概念,当然这只是我自己的理解。
补充知识:函数是一等公民的理解
js中函数是一等公民代表什么意思?
js中的函数是一等公民代表,函数在js中的使用是非常灵活的。具体体现在如下三个方面
(1)函数可作为参数传递给其他函数
(2)函数可作为返回值–让函数去生成一个函数
(3)函数可赋值给变量