1、前言

JavaScript作为一种广泛应用的编程言语,其特有的履行环境和效果域规矩给开发者带来了许多共同的概念和应战。本文将深入探讨JavaScript中的调用栈、效果域链和闭包,并经过代码示例具体论述它们之间的联系和应用。

2、调用栈(Call Stack)

当程序履行函数调用时,核算机会运用调用栈来办理函数的履行次序和联系。让咱们经过一个简略的示意图和说明来了解这个进程。

首要,假定咱们有以下两个函数:main() 和 function_A()。

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

|           |
|  main()   |  <- 栈顶
|___________|

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

| function_A() |  <- 栈顶
|   main()     |
|______________|

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

|   ...       |  <- 栈顶
|  function_B()|
|  function_A()|
|    main()    |
|______________|

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

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

|  function_A()|  <- 栈顶
|    main()    |
|______________|

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

|  main()      |  <- 栈顶
|______________|

这样,调用栈就能够有效地办理函数调用的次序和联系,保证程序能够正确地回来到函数调用点。再看下面代码:

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 词法效果域和词法环境

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

// 大局效果域
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 效果域链

function bar() {
    console.log(myName); // 输出'小陈'
}
function foo() {
    var myName = '小赵'
    bar()
}
var myName = '小陈'
foo()

在这段代码中,函数 barfoo 都是在大局效果域内声明的。因而,它们的外层效果域都是大局效果域。

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

这个比如再次说明晰词法效果域和效果域链的作业原理:函数的效果域是在函数界说的时候确认的,而不是在函数调用的时候;同时,效果域链的机制使得函数能够拜访到外部效果域中的变量。

4. 闭包(closure)

闭包是指在某个效果域内界说的函数,能够拜访该效果域内的变量,而且在函数界说的效果域外部被调用时依然能够运用这些变量。闭包使得函数能够保留对界说时效果域的拜访,即使函数在不同的效果域中被调用也依然有效。

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的拜访能力。

再看一段代码:

    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目标。
var bar = foo()
  1. var bar = foo(): 调用函数foo并将其回来值赋给变量bar
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. 了解难度: 关于初学者来说,闭包可能会增加代码的复杂度和了解难度,特别是在处理效果域链和变量生命周期时。

因而,在运用闭包时,需求权衡其优点和缺陷,合理利用闭包的优点,防止其潜在的缺陷,以保证代码的可维护性和功能。


以上便是今日的全部内容了~