闭包的深入了解,简单明了的理解闭包的原理

125 阅读4分钟

JavaScript中的闭包是一个非常重要且强大的概念。它使得我们能够在函数内部创建一个独立的作用域,同时仍然可以访问外部作用域中的变量。闭包不仅可以帮助我们实现私有化变量,还可以用于封装和模块化代码。但同时,闭包也可能导致内存泄漏的问题,因为它会持续保留对外部作用域中变量的引用,如果不注意及时释放,就会占用过多的内存空间。

调用栈

在理解闭包的概念之前,我们先来看一下调用栈。调用栈是用来管理函数调用关系的一种数据结构。 在JavaScript中,每当函数被编译时,都会创建一个称为“执行上下文”的内部对象,并被压入调用栈的底部。

image.png

当函数执行结束后,执行上下文将被弹出调用栈。清除函数栈的内存。(叉代表清除函数出栈)

image.png

这种调用栈的机制使得函数调用能够按照预期顺利进行。
若是函数执行完成却没有清除并反复调用就会爆栈:

function foo() {
    foo()
}
foo()
//函数爆栈

作用域链

作用域链是另一个重要的概念,它通过词法环境来确定某个作用域的外层作用域。在JavaScript中,作用域链决定了变量的查找路径,即变量由内而外的这种链状关系。当一个函数内部访问某个变量时,JavaScript引擎会根据作用域链来查找变量的值。

在JavaScript里面会存在一个变量outer,当函数被编译outer被赋的值就是函数的词法环境,它会指向外层作用域,当outer在全局作用域时outer=null。
如下图foo()里面存在outer用来指向全局作用域,bar()里面存在outer也用来指向全局作用域,当函数执行时会通过outer直接去全局作用域寻找变量。

image.png

闭包

闭包则是基于作用域链的概念而来的。在JavaScript中,内部函数总是能够访问外部函数中的变量。当通过调用一个外部函数返回一个内部函数后,即使外部函数已经执行完毕,但是内部函数引用了外部函数中的变量,这些变量依然会保存在内存中,形成了闭包。闭包使得内部函数可以继续访问外部函数的变量,即使外部函数已经执行结束。
如下图调用栈解释代码

function foo(){
    var myName = '阿美'
    let test1 = 1
   const test2 = 2
   var innerBar = {
    getName:function(){
      console.log(test1);
      return myName
    },
    setName:function(newName){
        myName = newName
    }
   }
   return innerBar
}
var bar = foo();
bar.setName("洋洋");
console.log(bar.getName());

当第16行代码foo的调用执行完毕,系统应该将foo从函数调用栈清除,但是第17行setName被调用时它的外层作用域是foo,那么setName的outer应该指向foo的词法环境,所以foo不能完全销毁,会留下一部分称为“包”,其余函数会调用这个包。

image.png

闭包的优点与缺点

优点:闭包有着诸多优点。最显著的优点是能够实现私有化变量。通过闭包,我们可以创建一个独立的作用域,从而隐藏一些变量,防止其被外部访问和修改,实现了数据的封装和保护。

缺点:然而,闭包也并非完美无缺。其中最主要的缺点是可能导致内存泄漏的问题。由于闭包会持续保留对外部作用域中变量的引用,如果不及时释放这些引用,就会占用过多的内存空间,导致内存泄漏。这意味着在使用闭包时,需要特别注意内存管理,及时释放不再需要的引用,以避免内存泄漏问题。

总结而言

闭包是JavaScript中一个非常强大的特性,能够帮助我们更好地组织和管理代码。然而,需要在使用闭包时注意内存管理,避免潜在的内存泄漏问题。通过合理地运用闭包,我们可以充分发挥其优点,同时避免其潜在的缺点,从而提升代码的质量和性能。