JavaScript 闭包:山重水复与柳暗花明

发布于:2024-05-08 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

js有“三座大山”,只要翻过这几座大山那就差不多精通js了。闭包就是js的“三座大山”之一,我们必须要翻过它。在 JavaScript的学习之旅中,有“三座大山”横亘在前,它们是深入理解和精通这门语言的关键所在。而闭包,正是其中一座极具挑战性的高峰。

尽管闭包可能会带来一些复杂的概念和理解上的困难,但只要我们持之以恒,不断钻研,就一定能够跨越它,领略到其背后那无比广阔的编程境界。

正文

什么是闭包?

闭包(closure)是一个函数以及其捆绑的周边环境状态的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

看这个定义后感觉懂了又没有懂。

用自己的话解释闭包:在JavaScript中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量;当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用。那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。

如果还是云里雾里的话可以继续阅读到下面的例子解释,或许你就会明白。

闭包的特性

  1. 函数嵌套函数。
  2. 函数内部可以引用函数外部的参数和变量。
  3. 参数和变量不会被垃圾回收机制回收,

了解闭包的过程

通过图解代码的执行过程了解闭包。(在图解过程中function表示函数体)

function father() {
    function son() {
        var age = 18
        console.log(myName);
    }
    var myName = "小明"
    return son
}
var myName = '大明'
var fun = father()
fun()
  1. 对全局代码进行预编译。

屏幕截图 2024-05-06 184940.png
  1. 执行全局代码,在执行到var fun = father()时调用father函数,停止执行全局代码。

屏幕截图 2024-05-06 190258.png
  1. 对father函数进行预编译。

屏幕截图 2024-05-06 190658.png

father函数的执行上下文内的outer指向father函数的词法作用域,也就是全局作用域。

  1. 执行father函数。

屏幕截图 2024-05-06 190854.png
  1. father函数执行完后,father函数的执行上下文需要删除吗?

    当相关代码执行完毕返回后,将正在运行的执行上下文从执行上下文栈删除。如果函数执行完毕后执行上下文不删除,会造成执行上下文栈删很快就会满。所以father函数的执行上下文应该删除。

    在这种情况下我们可以分为2个情况,我们分析分析哪个是对的。

    • 情况一:删除father函数的执行上下文后无其他后续。

      预编译son函数。

屏幕截图 2024-05-06 204319.png

你会发现,son函数的变量环境的outer没有指向,因为son的词法作用域(father的作用域)被撤销了。这样son作用域甚至不能通过作用域链访问全局作用域,也就是作用域链直接断开了。那这段代码就会报错,但是结果并不是这样的。

结果是:

屏幕截图 2024-05-06 210803.png

首先,当相关代码执行完毕返回后,将正在运行的执行上下文从执行上下文栈删除;这是铁律,是不会出错的。那问题会在哪呢?

这就说明在删除father函数的执行上下文时存在一些其他的操作。证明这个猜想是错误的。

  • 情况二:删除father函数的执行上下文后有其他后续。

    这时候就需要提到闭包。我们用个小故事理解接删除father函数的执行上下文时的一些其他的操作。

    son因为叛逆离家出走,多年不回家。因为一些变故需要搬家,father非常想找到son。如果搬家了,那son就会找不到他们了。那该怎么办呢?这时father想到了一个方法,那就是将father新家的地址信息放在一个盒子里留在房子里。如果son回到房子就能找到需要的信息。

    然而这个盒子就是闭包。

屏幕截图 2024-05-06 213154.png

为什么father执行上下文没有被完全移除,并且留下了一个小盒子(闭包)?

因为father自己都不知道自己有没有执行完。father返回了一个son函数,如果调用了son函数就会找不到需要的东西;为了防止发生错误,于是留下一个小盒子(闭包)只保留son会用到的东西,其余的father的东西都销毁。

我们可以清楚得知道,闭包里面会保存有可以被son函数引用的变量myName='小明'和outer(指向father的词法作用域)。保存father函数原来的outer是为了保护作用域链不断裂。

  1. 执行son函数,可以得到输出结果为小明

小tip

outer指向的是词法作用域。词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。

eg:

let count = 1

function main() {
    let count = 2

    function bar() {
        let count = 3

        function foo() {
            let count = 4
        }
    }
}

Snipaste_2024-05-06_15-17-05.jpg

运用闭包

eg:实现一个自增计数器,用闭包实现。

function add() {
    let count = 0;
    function fn() {
        count++;
        return count;
    }
    return fn
}

var res = add()

console.log(res());   //1
console.log(res());   //2
console.log(res());   //3
  1. 在 add 函数内部,定义了变量 count 和函数 fn。
  2. fn 函数可以访问并修改 count变量,形成了闭包。
  3. 当执行 add函数时,返回了内部的 fn 函数。
  4. 后续通过 res 调用返回的 fn时,fn 仍然能够访问到 add 函数作用域中的 count 变量,并对其进行操作和返回。
  5. 每次调用 res,count 都会递增,这体现了闭包对变量的持久保存和访问。

小结

JavaScript 中的闭包是一个复杂但重要的概念。它是一个函数以及其捆绑的周边环境状态的引用的组合。当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,内部函数仍能通过闭包引用外部函数的变量。闭包具有函数嵌套函数、函数内部可引用外部参数和变量以及参数和变量不会被垃圾回收机制回收等特性。了解闭包可以通过图解代码执行过程来实现,比如通过分析函数的预编译和执行过程来理解闭包的作用。闭包在实际编程中有很多应用,如实现自增计数器等。掌握闭包对于深入理解 JavaScript 语言的运行机制和实现复杂功能非常重要。