了解令大多数JavaScript学者头疼的知识!闭包

338 阅读7分钟

前言

在学习闭包前,我们先来解释一下闭包是什么,有什么作用,作为学习小白,可能我们看各种教科书对于闭包概念的讲解都不能清楚的知道闭包到底是个什么东西,今天就让我们一起来深入分析探讨闭包知识,看完这篇文章,你对闭包的了解便更上一层楼!

闭包的概念及作用

闭包是指在编程中,一个函数可以访问其外部函数作用域中的变量,即使在该外部函数已经执行完毕的情况下仍然可以访问这些变量。这种行为常见于函数式编程语言中,可以用来实现某些特定的功能或者模式。

闭包有几个主要的作用:

  1. 保留状态:闭包可以用来保留函数运行时的状态信息,使得函数可以记住其创建时的环境。
  2. 封装:闭包可以将函数与其相关的数据封装在一起,形成一个整体,有利于代码的模块化和组织。
  3. 访问外部变量:闭包可以访问其外部函数作用域中的变量,使得在函数外部无法直接访问的变量也可以被引用和操作。
  4. 延迟执行:闭包可以延迟对函数的执行,使得函数可以在其被返回后的任意时刻被调用执行。
  5. 实现回调和事件处理:闭包可以用来实现回调函数和事件处理函数,使得函数可以在特定事件发生时被调用执行。

总的来说,闭包提供了一种灵活而强大的机制,可以在函数式编程中实现许多有用的功能。

调用栈以及作用域链

在讲解闭包之前,我们需要先了解一下什么是调用栈以及作用域链。

调用栈

调用栈:调用栈(call stack)是计算机科学中用于跟踪函数调用的一种数据结构。当一个函数被调用时,它的信息(如参数值、返回地址等)被压入调用栈的顶部。当函数执行完毕时,它的信息从栈顶弹出,控制权返回给调用该函数的地方。这种机制确保了函数调用的顺序和正确性。编译器在编译时会首先创建一个栈来存放数据。

111.jpg

在栈中遵循先进后出的原则,所以1最先进来最后出去,4最后进来最先出去,且一个栈的容量是有限的,若代码编写不对,则可能会爆栈。用如下一段代码来掩饰入栈:

var a = 2
function add(){
   var b = 10
   return a+b
}
add()

编译开始时,首先在调用栈中创建全局执行上下文,其中包含变量环境词法环境,变量环境中包含全局中var声明的变量以及声明的函数,而let以及const声明的变量则放在词法环境中。

全局编译结束后,便开始执行,执行函数add()前又要进行函数体编译,此时又会在栈中创建add执行上下文,其处在全局上下文之上,因为全局先入栈,add后入栈,add执行上下文中也包括变量环境和词法环境,当add()执行完毕时,栈中便会清理掉add执行上下文,全部代码执行完毕后,栈内便空了,以保持栈中空间,因此调用栈便是各类执行上下文短暂存储的空间。

作用域链(Scope Chain)

作用域链(Scope Chain)是在 JavaScript 中用于解析标识符的机制。它是由当前执行上下文的作用域及其所有外部包含(父级)作用域组成的链式结构。当引用一个变量时,JavaScript 引擎会沿着作用域链向上查找,直到找到匹配的标识符或到达全局作用域为止。

现在我们通过一段代码来了解什么时作用域链:

function bar(){
    console.log(myname);
}
function foo(){
    var myname ='曦哥'
    bar()
}
var myname='晨哥'
foo()

从此段代码中,我们可以看到,bar()函数体中有个console.log(myname),需要输出其myname的值,根据查找规则,从内向外查找(前面解释作用域时有提到查找规则),此时在bar()中找不到myname的值,于是就需要去bar()函数外层作用域找,外层作用域就是outer指向的作用域,也叫词法环境(在哪定义的该函数词法环境就在哪),所以此时bar()中找不到myname就要去全局中找,因为bar()函数在全局中被定义,所以在全局中找到了myanme=‘晨哥’,所以console.log(myname)输出的值为‘晨哥’。

因此我们把从bar()到全局中查找的这种链状关系叫做作用域链。

闭包

我们看闭包的概念并不能有效的理解闭包到底是个什么东西,所以我们就通过一段代码来理解闭包

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());
123.jpg

如上如所示,蓝色的是全局是上下文,黄色的是foo()执行上下文,黑色箭头为作用域链,当将innerbar返回给bar时,foo()函数执行结束,所以黄色的执行上下文要清除出栈,此时执行bar.setname,调用setname函数,则setname执行上下文入栈。

b120cedc71ef8242443d10224949780.jpg

setname中,要newname(洋洋)赋值给myname,所以我们得知道myname的声明,但myname的声明在foo()中,foo()函数执行上下文已出栈,找不到myname,那么现在就轮到我们今天的主角闭包出现了!

为了setname可以顺利找到myname,foo()执行上下文在清理出去的同时会留下一个包,里面包含了后续需要利用的变量声明,例如题中需要的 test1=1 以及myname=‘阿美’,而这个包就叫做闭包

0d92b9a1080d7863c3cfc813d3e8a35.jpg

所以结果我们也可以猜到,setname函数成功找到myname的声明及定义,并将myname的值从阿美改成了洋洋,且找到了test1=1,下面我们来看执行结果。

image.png

闭包的优缺点

到学习的最后,我们来补充一下闭包的一些优缺点,不是说闭包可以随意使用,闭包在 JavaScript 中是一种强大的特性,但也有一些优点和缺点。

优点:

  1. 封装性和隔离性:  闭包可以将变量和函数封装在一个作用域内,避免了全局命名空间的污染,提高了代码的隔离性和安全性。
  2. 保持状态:  闭包可以在外部函数执行完毕后,仍然保持对外部函数作用域的引用,从而可以持续访问和修改外部函数的变量,实现状态的保持。
  3. 实现函数式编程:  闭包使得 JavaScript 可以支持函数式编程的特性,例如高阶函数、柯里化等,使得代码更加灵活和抽象。

缺点:

  1. 内存泄漏:  由于闭包会保持对外部函数作用域的引用,如果闭包未被正确释放,可能导致内存泄漏,造成不必要的内存消耗。
  2. 性能问题:  闭包会增加内存和执行上下文的开销,尤其是在嵌套过深或循环中频繁使用闭包时,可能会导致性能问题。
  3. 可维护性降低:  过度使用闭包可能会导致代码结构变得复杂,降低代码的可读性和可维护性,特别是对于不熟悉闭包概念的开发人员来说。

因此,在使用闭包时,需要权衡其优点和缺点,并注意避免潜在的问题,以确保代码的可靠性和性能。

今天的学习就到这啦,希望小编的文章对大家的学习有所帮助,谢谢大家!