三. 闭包

116 阅读5分钟

三. 闭包

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

  1. 执行代码

    1. 执行前先会创建全局执行上下文,然后预解析,将foo函数提升,因为是函数, 会自动在堆内存开闭一个空间(假设内存地址是0xa01),用于储存foo函数,然后变量fn提升,

    2. 执行到foo()时,发现是个函数,会在函数执行栈里面创建函数执行上下文,创建后,不会立即调用函数里面的内容,而是先创建AO对象,然后对foo解析,发现bar是函数,会自动在堆内存开闭一个空间(假设内存地址是0xa02),用于储存bar函数。然后才执行 return bar。到这里foo函数就已经执行完了,函数执行上下文被弹出,AO对象被销毁。在全局执行上下文里执行var fn = foo()时,其实是将bar的内存地址赋值给了fn。

    3. 执行fn(),此时的fn保存的是bar的地址,就是执行bar(),执行前,先创建函数执行上下文,生成AO,因为没有东西提升,所以是个空对象,然后执行代码,然后函数执行上下文出栈,AO被销毁,

image-20220212182810133-16547768522972.png

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.执行代码

  1. 执行前先会创建全局执行上下文,然后预解析,将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即可

image-20220215110053656.png

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引擎删除了

image-20220215125225835.png