【2019】理解JavaScript闭包,从其作用域链开始

224 阅读5分钟

俗话说好记性不如烂笔头,最近越发觉得这句话对于学习一门编程语言的重要性,所以开始尝试去做这件事。同时也把它分享出来和大家一起学习。笔者水平有限,所总结的知识点都是站在前人的肩膀上,所以如果有抄袭的嫌疑请原谅,我会在文章最后贴出参考的文章连接。如果有错误的地方请帮我指出,谢谢!

1、理解闭包

概念

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数。所以对闭包简单的理解就是函数内的函数. 当带有闭包逻辑的函数在执行完毕后,父函数定义的变量在子函数的作用域链中,子函数没有被销毁,其作用域链中所有变量和函数就会被维护,不会被销毁。下面就是一个闭包函数的例子,其中的匿名函数就是闭包

function foo(a){    
    return  function (b){
        return a + b;
    }  
}

作用域链

讲到闭包就一定要了解JavaScript中作用域链的概念,如何创建作用域链以及作用域链有什么作用的细节,对彻底理解闭包至关重要。那么什么是作用域链呢?

       在JavaScript高级程序设计(第三版)中介绍到:当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链中的下一个对象来自包含(外部)环境,而下一个变量对象则来自下一个包含环境,这样,一直延续到全局执行环境,对象在作用域链由下网上查找,直到全局对象。下面我们通过具体的代码的编译过程来理解它,在代码开始之前我们先来了解两个概念:

  • [[Scope]]    这是一个仅供JavaScript·引擎使用的函数内部属性,当函数被创建时,这个内部属性就会包含函数被创建的作用域中对象的集合 ,这个集合呈链式链接,也就是我们所说的作用域链
  • Variable Obejct(VO):作用域链上的每一个对象,包含全局对象GO(Global Object)和活动对象AO(Active Object) 

var g = 3;
function foo(){
    var a = 1
     function bar(){
        var b = 2;
    }
    bar();
}
foo();

下面我们上面代码的执行过程来理解作用域链

  • 1.在foo()创建的时候,foo函数被自动赋予了[ [ scope ] ]属性,只不过这个属性我们看不到。并且该函数的全局对象GO也被推入了作用域对象的集合
  • 2. 在函数foo执行的时候,通过预编译产生的活动对象AO,会被推入到该作用域集合的顶端,集合中的第0位变成了自己的活动对象AO ,此时就形成了作用域链,即函数foo在执行的时候加入了自己的作用域

  • 3. foo函数在被编译的同时也创建了bar函数,所以bar函数也会自动赋予[ [ scope ] ]属性,又应为它是foo的内部函数,所以它的作用域链与foo函数在执行时的一样

  • 4.bar函数在执行的时候也会产生自己的活动对象AO,也会被推入到该作用域链的顶端


  • 5 另外我们知道,函数在执行的时候会产生自己的执行环境(execution context),它其实也有自己的作用域链,并且它是复制了[[Scope]]属性保存的作用域链,只不过[[Scope]]属性是函数创建时产生的,会一直存在 而执行上下文在函数执行时产生,函数执行结束便会销毁
  • 6.通过上面bar函数执行时的作用域链我们可以看到,如果编译器需要去寻找全局变量g,那么它将会从其作用域链上一层一层往上寻找,即从scope chain集合0开始,一直寻找到最后一个2,即全局对象GO,假如没有找到那么就会报错了

       如果以上你觉得还是难懂的话,那我们换种说法。 当我们调用一条数据的时候,JavaScript首先会在当前作用域中进行查找,如果找不到,就向上找到父级的作用域,如果在父级的作用域中也找不到,就继续向上查找,直到window的作用域。如果在window中也找不到,就报错了,这种查找规则就是建立在JavaScript的作用链上的。下图画出了我们刚才闭包示例代码中的作用域链 ,其中每种颜色都是一种执行环境也就是作用作用域链与闭包的关系

       一般情况下,函数执行完毕后,其局部变量就会被销毁,内存中仅保存全局变量,但是闭包的情况又有所不同。在我们的闭包例子中当foo函数在执行完毕后,父函数的局部变量a是不会被销毁的,因为子函数的作用域链仍在引用父函数的局部变量。换句话说,当子函数返回后,其执行环境的作用域链会被销毁,但他的局部变量仍会留在内存中,直到子函数被销毁后,父函数的局部变量a才会销毁。因此闭包也会比其他函数占用更多的内存。用刚才[[scopr]]属性来理解我们刚才的例子就是返回出的子函数的[[scope]]中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包


未完待续。。。。


2、闭包的主要作用

  • 模仿块级作用域
  • 在对象中创建私有变量

3、使用闭包的注意事项

  • 闭包与变量
  • this在闭包中
  • 内存泄露

4、参考资料