三. 闭包
3.1. 闭包的定义
这里先来看一下闭包的定义,分成两个:在计算机科学中和在JavaScript中。
-
在计算机科学中对闭包的定义(维基百科):
- 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures);
- 是在支持 头等函数 的编程语言中,实现词法绑定的一种技术;
- 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
- 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量 会在补充时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;
-
闭包的概念出现于60年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么JavaScript中有闭包:
- 因为JavaScript中有大量的设计是来源于Scheme的;
-
我们再来看一下MDN对JavaScript闭包的解释:
- 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure);
- 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;
- 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
-
那么我的理解和总结:
- 一个普通的函数function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包;
- 从广义的角度来说:JavaScript中的函数都是闭包;
- 从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用于的变量,那么它是一个闭包;
3.2. 嵌套函数执行过程
function foo() {
function bar() {
console.log("bar");
}
return bar
}
var fn = foo()
fn()
0. 创建go
-
执行代码
-
执行前先会创建全局执行上下文,然后预解析,将foo函数提升,因为是函数, 会自动在堆内存开闭一个空间(假设内存地址是0xa01),用于储存foo函数,然后变量fn提升,
-
执行到foo()时,发现是个函数,会在函数执行栈里面创建函数执行上下文,创建后,不会立即调用函数里面的内容,而是先创建AO对象,然后对foo解析,发现bar是函数,会自动在堆内存开闭一个空间(假设内存地址是0xa02),用于储存bar函数。然后才执行 return bar。到这里foo函数就已经执行完了,函数执行上下文被弹出,AO对象被销毁。在全局执行上下文里执行var fn = foo()时,其实是将bar的内存地址赋值给了fn。
-
执行fn(),此时的fn保存的是bar的地址,就是执行bar(),执行前,先创建函数执行上下文,生成AO,因为没有东西提升,所以是个空对象,然后执行代码,然后函数执行上下文出栈,AO被销毁,
-
3.3. 闭包的执行过程_ 内存泄漏
function foo() {
var name="foo"
var age= 18
function bar() {
console.log(name);
console.log(age);
}
return bar
}
var fn = foo()
fn()
1.创建go
2.执行代码
- 执行前先会创建全局执行上下文,然后预解析,将foo函数提升,因为是函数, 会自动在堆内存开闭一个空间(假设内存地址是0x001),用于储存foo函数,然后变量fn提升,然后执行代码,发现bar是函数,会开闭一个新空间,用于保存函数对象,这时候bar的作用域和父级作用域就被确定了,并且在堆中的[[scope]]中指向,若bar引用了foo的变量,foo函数执行上下文被弹出时,foo的AO对象会被保留下来;反之则被销毁。然后执行fn(),也就是bar函数,生成AO对象,由于没有要提升的东西,所以里面为空,然后弹出bar,对应的AO也被销毁
如果后面在执行foo(),可不可以呢?肯定是可以的,foo函数对象一直存在,把foo函数对象放到调用栈去创建函数执行上下文就行了,如果后面在执行bar(),也是一样的,把bar函数对象放到调用栈去创建函数执行上下文就行了,
问:为什么说闭包可能会有内存泄漏?
答:因为bar函数一直不会被销毁,原因是fn一直执行着bar函数对象,bar函数不销毁,那bar的父级作用域(foo的AO)也就不会销毁,如果bar一直需要用,则没有内存泄漏, 假如用一次就不再需要bar函数,那堆一直保存着bar函数对象和foo的AO对象就没有意义了,我们把这部分一直占用的内存称为内存泄漏
问:如何解决内存泄漏
答:fn= null;将fn指向null的内存地址,然后根据GC的标记清除,从根(GO)开始查找,有引用指向的对象不会被销毁, foo指向foo函数对象,所以不会销毁; foo的AO和bar函数对象虽然形成了循环引用,但根对象永远指不向他们,根据GC的标记清除,下一次检测中,他们会被销毁
如何连foo函数也不能需要了,直接foo= null即可
3.4. AO对象中未使用的变量的销毁
function foo() {
var name="foo"
var age= 18
function bar() {
debugger
console.log(name);
}
return bar
}
var fn = foo()
fn()
问:foo的AO对象没有被销毁,那么age属性会被销毁吗?
答:会 当执行到debugger时,在浏览器中会暂停,可以看到变量在内存中的一些表现, 如图foo函数是个闭包(Closure),但age由于没有被引用,已经被V8引擎删除了