闭包难以理解?看完这些你也能轻松拿捏。

534 阅读5分钟

闭包与作用域是JavaScript中两个核心且紧密相关的重要概念,它们共同构成了这门动态语言在处理变量和函数时的复杂而又强大的机制。本文旨在深入浅出地解析闭包的概念、作用及其潜在的缺点,并探讨其与作用域链之间的关系,作用域的相关内容在我的上一篇文章中有所介绍,不过没关系这里我也有详细解读。

在了解闭包之前我们要知道下形成闭包的天然条件:作用域链

  1. 作用域链js 引擎在查找变量时会先在函数中查找,找不到就会根据outer的指向去到外层作用域中查找,层层往上,这种查找的关系链就称为 作用域链。
  2. 我们可以分析一下下面代码来理解
function bar() {
            var myname = 'Tom'    
            let test1 = 100             
            if (1) {
              let myname = 'Jerry'     
              console.log(test,myname);//打印的结果为:1,Jerry
            }    
          }  
function foo() {
            var myname = '彭于晏'     
            let test = 2              
            {
              let test = 3           
              bar()
            }
          }
var myname = '晟哥'      //全局执行上下文中时的变量环境 
let test = 1              //全局执行上下文中时的词法环境
foo()

通过观察我们知道函数foobar都是声明在全局的,那么它们的词法作用域都是指向全局。画出具象化图形如下: image.png 如上:我们也可以通过画图来加深理解
V8引擎在查找变量在词法环境再到变量环境,如果找不到则会根据outer的指向去到外层作用域中查找,而outer的外层作用域是由是由函数声明的位置来决定的,bar函数是声明在全局中的,故此打印的结果为:1,Jerry。

如果上面代码你看懂了,那么恭喜你掌握了作用域链。

接下来我们就可以开始探索闭包了。

# 闭包

根据JS词法作用域的规则(上一篇文章又详解),内部函数总是能访问外部函数中的变量,当通过调用一个外部函数返回的一个内部函数后,即使外部函数执行已经结束了,但内部函数引用了外部函数中的变量也依旧要保存在内存中,我们把这些变量的集合叫做闭包。

文字确实有些抽象了,那我们就用一段代码来理解。如下

function foo() {
    var name = '大仙'
    function bar() {
        console.log(count, age)
    }
    var count = 1
    var age =  18
    return bar
}
var age = 20
const baz = foo()
baz()

按照词作用域链来理解上面输出的结果是应该是undefined,20。foo函数执行完应该在调用栈中被清理才对,但是他声明的变量却没有被清理掉。在结合刚才对闭包的介绍:内部函数引用了外部函数中的变量也依旧要保存在内存中,我们把这些变量的集合叫做闭包。 image.png 如上:我们也可以通过画图来加深理解。在foo执行完后其声明的变量因为被bar函数引用的原因而被保存在了一个调用栈的小背包里。

如果你看懂了上面代码,那么恭喜你掌握了闭包。

现在我们来看下面代码

var arr = []
​
for (var i = 0 ; i < 10; i++){
        arr[i] = function(){
            console.log(i);
        }                
}
arr.forEach(function(item) {
    item()
})
​

仔细分析你会发现输出的结果为打印了10次10。如果要不改变原有的数据结构,实现打印出0~9。你如何实现

聪明的你肯定能想到把第二行的var改为let 使得每次循环都会创建一个新的块级作用域,每个作用域内的i都是独立的。因此,每个函数都会输出其对应的i值,从0到9依次打印。除了这个方法你还能想到什么方法吗?刚学完,你肯定也会使用闭包。如下:

var arr = []
​
for (var i = 0 ; i < 10; i++){
    (function(i){
        arr[i] = function(){
            console.log(i);
        }
    })(i)   // 添加括号,确保立即执行                   
}
arr.forEach(function(item) {
    item()
})
​

这段代码通过闭包机制和立即执行函数表达式巧妙地解决了循环中变量共享的问题,确保了每个函数都能准确地记录并输出它在创建时刻循环变量i的值。

总结

  1. 闭包的定义:闭包是指内部函数能够访问外部函数中的变量,并且在外部函数执行结束后,内部函数依然可以引用外部函数中的变量。这些变量的集合被称为闭包。

  2. 闭包的优点

    • 封装性:闭包允许创建私有变量,有助于封装和隐藏信息,减少全局变量的使用,从而减少命名冲突,提高代码的模块化程度。
    • 持久性状态:闭包可以维持函数内部的状态,即使在外部函数执行完毕后,内部函数仍能继续访问和修改这些状态,这对于创建计数器、缓存等场景非常有用。
    • 实现封装和信息隐藏:通过闭包,开发者可以创建公共接口(例如,返回特定的函数),同时隐藏实现细节和内部状态,这是面向对象编程中的一个重要概念。
    • 模块化:闭包可以用来模拟类和对象,实现模块模式,有助于组织和复用代码。
  3. 闭包的缺点

    • 内存泄漏:闭包会导致外部函数中的变量在内部函数引用后仍然被保留在内存中,可能造成内存泄漏,使得栈的可用空间变小。需要注意释放不再使用的变量,以避免内存泄漏问题。
    • 调试困难:闭包增加了代码的复杂度,尤其是在大型项目中,跟踪和理解闭包中的数据流向和生命周期可能变得更加困难。

总的来说,闭包是JavaScript中的一个重要概念,能够提供一种灵活的方式来管理变量和实现模块化开发。但同时,需要注意闭包可能导致的内存泄漏问题,合理地使用闭包并注意内存管理,可以更好地利用闭包带来的便利和优势。