小白一看就懂!揭秘JavaScript中神奇的闭包机制

1、前言

JavaScript作为一种广泛应用的编程语言,其特有的执行环境和作用域规则给开发者带来了许多独特的概念和挑战。本文将深入探讨JavaScript中的调用栈、作用域链和闭包,并通过代码示例详细阐述它们之间的关系和应用。

2、调用栈(Call Stack)

当程序执行函数调用时,计算机会使用调用栈来管理函数的执行顺序和关系。让我们通过一个简单的示意图和说明来理解这个过程。

首先,假设我们有以下两个函数:main() 和 function_A()。

当程序开始执行时,main() 函数会被调用,它的执行环境会被压入调用栈中,如下图所示:

markdown 复制代码
|           |
|  main()   |  <- 栈顶
|___________|

接着,假设 main() 函数内部调用了 function_A() 函数,那么 function_A() 的执行环境就会被压入调用栈,如下图所示:

markdown 复制代码
| function_A() |  <- 栈顶
|   main()     |
|______________|

现在,function_A() 函数内部又调用了其他函数,那么新的执行环境会被依次压入调用栈,形成类似下面这样的结构:

scss 复制代码
|   ...       |  <- 栈顶
|  function_B()|
|  function_A()|
|    main()    |
|______________|

注意到调用栈是一个后进先出(LIFO)的数据结构,因此函数执行结束后,它们的执行环境会从调用栈中弹出,控制流会回到调用该函数的位置继续执行。

当 function_B() 执行完成后,它的执行环境会被弹出,调用栈变为:

markdown 复制代码
|  function_A()|  <- 栈顶
|    main()    |
|______________|

最后,当 function_A() 也执行完成后,它的执行环境会被弹出,调用栈变为:

markdown 复制代码
|  main()      |  <- 栈顶
|______________|

这样,调用栈就能够有效地管理函数调用的顺序和关系,确保程序能够正确地返回到函数调用点。再看下面代码:

scss 复制代码
function firstFunction() {
    secondFunction();
}

function secondFunction() {
    thirdFunction();
}

function thirdFunction() {
    console.log("Hello third function!"); // 输出"Hello third function!"
}

firstFunction();

在这段代码中,当调用firstFunction()时,它又调用了secondFunction(),然后secondFunction()又调用了thirdFunction()。在这个过程中,每次函数调用都会创建一个新的活动记录,并被推入调用栈的顶部。

所以调用栈的情况如下:

  1. 首先,firstFunction()被调用,因此它的活动记录被推入调用栈的顶部。
  2. 接着,secondFunction()被调用,它的活动记录被推入调用栈的顶部。
  3. 然后,thirdFunction()被调用,它的活动记录被推入调用栈的顶部。
  4. thirdFunction()执行完毕后,它的活动记录从调用栈中弹出。
  5. 接着,secondFunction()执行完毕后,它的活动记录也从调用栈中弹出。
  6. 最后,firstFunction()执行完毕后,它的活动记录也从调用栈中弹出。

最终,调用栈变成了空栈。

这个例子演示了函数调用时如何在调用栈中进行推入和弹出活动记录的过程。

3、 作用域链(Scope Chain)

作用域链描述了确定某个作用域的外层作用域的过程,它通过词法环境来实现。在JavaScript中,作用域链决定了在何处可以访问变量。当某个变量在当前作用域中无法找到时,JavaScript引擎会沿着作用域链向外查找,直至找到该变量或达到全局作用域。这种机制使得内部作用域可以访问外部作用域的变量,形成了一种作用域嵌套的结构。

3.1 词法作用域和词法环境

讲到作用域链,我们通常也会讲到词法环境和词法作用域,如果有小白不清楚这三者有什么区别,那么请看下面代码:

javascript 复制代码
// 全局作用域
let globalVariable = 'global';

function outerFunction() {
  // outerFunction 作用域
  let outerVariable = 'outer';

  function innerFunction() {
    // innerFunction 作用域
    let innerVariable = 'inner';
    console.log(innerVariable);  // 在当前词法环境中找到 innerVariable
    console.log(outerVariable);  // 在外部词法环境中找到 outerVariable
    console.log(globalVariable);  // 在全局词法环境中找到 globalVariable
  }

  innerFunction();
}

outerFunction();

在这段代码中,我们可以看到:

  • globalVariable 是在全局作用域中声明的全局变量。
  • outerFunction 是一个函数,它创建了一个词法环境,并在其中声明了 outerVariable 这个变量。
  • innerFunctionouterFunction 中的内部函数,它创建了另一个词法环境,并在其中声明了 innerVariable 这个变量。

当代码执行时,JavaScript 引擎会根据词法作用域和作用域链的规则来查找和访问变量。在内部函数 innerFunction 中,它可以直接访问到自身词法环境中的变量 innerVariable,也可以通过作用域链找到外部词法环境中的变量 outerVariable 和全局词法环境中的变量 globalVariable。 这个例子展示了词法环境、词法作用域和作用域链在 JavaScript 中的基本工作原理。希望这能够帮助你更好地理解它们之间的关系。

3.2 作用域链

javascript 复制代码
function bar() {
    console.log(myName); // 输出'小陈'
}

function foo() {
    var myName = '小赵'
    bar()
}
var myName = '小陈'
foo()

在这段代码中,函数 barfoo 都是在全局作用域内声明的。因此,它们的外层作用域都是全局作用域。

当调用 foo 函数时,它内部声明了一个名为 myName 的变量,并赋值为 '小赵',然后调用了函数 bar。在函数 bar 中尝试打印 myName 变量时,并没有在当前函数的词法环境中找到对应的变量,于是 JavaScript 引擎根据作用域链向上查找,在全局作用域中找到了 myName 变量,其值为 '小陈',因此输出为 '小陈'

这个例子再次说明了词法作用域和作用域链的工作原理:函数的作用域是在函数定义的时候确定的,而不是在函数调用的时候;同时,作用域链的机制使得函数可以访问到外部作用域中的变量。

4. 闭包(closure)

闭包是指在某个作用域内定义的函数,可以访问该作用域内的变量,并且在函数定义的作用域外部被调用时仍然可以使用这些变量。闭包使得函数可以保留对定义时作用域的访问,即使函数在不同的作用域中被调用也依然有效。

javascript 复制代码
function outerFunction() {
  var outerVariable = 'I am from the outer function';
  
  function innerFunction() {
    console.log(outerVariable); // 内部函数引用了外部函数的变量
  }
  
  return innerFunction; // 返回内部函数
}

var innerFunc = outerFunction(); // 调用外部函数并将内部函数赋值给innerFunc
innerFunc(); // 虽然outerFunction已经执行完毕,但innerFunction仍然可以访问outerVariable

在这个例子中,innerFunction形成了闭包,因为它引用了外部作用域中的变量outerVariable,并且在外部作用域之外被调用时仍然能够访问到这个变量。这种特性使得innerFunction能够"记住"它被创建时的环境,即outerFunction中的作用域。

通过以上代码,我们可以清晰地看到闭包的特点:内部函数innerFunction可以访问外部函数outerFunction的变量outerVariable,并且在外部函数执行完毕后仍然保持对outerVariable的访问能力。

再看一段代码:

function 复制代码
    var myName = '阿美'
    let test1 = 1
    const test2 = 2

    var innerBar = {
        getName: function() {
        console.log(test1); //输出 1
        return myName
        },
        setName: function(newName) {
            myName = newName
        }
    }
    return innerBar;
}
var bar = foo()
bar.setName('洋洋')
console.log(bar.getName()); 输出 '洋洋'
  1. function foo() {: 定义一个名为 foo 的函数。
  2. var myName = '阿美': 创建一个变量 myName,并将字符串 '阿美' 赋给它。
  3. let test1 = 1: 使用 let 关键字创建一个块作用域的变量 test1,并将值 1 赋给它。
  4. const test2 = 2: 使用 const 关键字创建一个块作用域的常量 test2,并将值 2 赋给它。
  5. var innerBar = { ... }: 创建一个对象 innerBar,包含了 getNamesetName 两个方法。
  6. getName: function() { ... }: 在 innerBar 对象中定义了一个 getName 方法,该方法打印出 test1 的值并返回 myName 的值。
  7. setName: function(newName) { ... }: 在 innerBar 对象中定义了一个 setName 方法,用于修改 myName 的值。
  8. return innerBar;: 返回 innerBar 对象。
ini 复制代码
var bar = foo()
  1. var bar = foo(): 调用函数 foo 并将其返回值赋给变量 bar
arduino 复制代码
bar.setName('洋洋')
console.log(bar.getName());
  1. bar.setName('洋洋'): 调用 bar 对象的 setName 方法,并将参数 '洋洋' 传递给它,从而修改了 myName 的值为 '洋洋'。
  2. console.log(bar.getName());: 打印 bar 对象的 getName 方法的返回值,即打印出 myName 的值。

综上所述,这段代码定义了一个函数 foo,该函数返回一个包含 getNamesetName 方法的对象,并且在调用过程中修改了 myName 的值。

4.1 闭包的优缺点

优点:

  1. 封装变量和函数: 闭包允许我们创建私有变量和函数,并且可以通过暴露公共接口来访问和修改这些私有内容,从而实现了信息隐藏和封装性。
  2. 保持状态: 闭包可以保持函数执行时的状态,即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。
  3. 实现模块化: 通过闭包,可以创建模块化的代码结构,将相关的变量和函数组合在一起,以便提高可维护性和复用性。
  4. 回调函数: 闭包经常用于创建回调函数,使得我们能够在异步操作中访问外部作用域的变量。

缺点:

  1. 内存占用: 由于闭包保持对外部作用域变量的引用,可能会造成内存泄漏,特别是在循环中使用闭包时需要格外小心。
  2. 性能问题: 闭包涉及到对外部变量的引用,可能会导致更多的作用域链查找,从而影响代码的执行效率。
  3. 理解难度: 对于初学者来说,闭包可能会增加代码的复杂度和理解难度,特别是在处理作用域链和变量生命周期时。

因此,在使用闭包时,需要权衡其优点和缺点,合理利用闭包的优点,避免其潜在的缺点,以确保代码的可维护性和性能。


以上就是今天的全部内容了~

相关推荐
橙子家21 分钟前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
前端
user205855615181326 分钟前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州28 分钟前
CSS aspect-ratio 属性完全指南
前端
Pedantic2 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘3 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆3 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师4 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆4 小时前
VSCode自动格式化三要素
前端
爱勇宝5 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员