likes
comments
collection
share

"JavaScript闭包:让你的代码更加灵活和高效"

作者站长头像
站长
· 阅读数 7

引言

这次我们来聊聊什么是闭包,闭包是一个非常重要的概念。在JavaScript中,闭包可以实现私有化变量、模块化编程等功能,但同时也可能会导致内存泄漏问题。再聊闭包之前,我们首先得谈谈什么是调用栈和作用域链。

调用栈

调用栈(Call Stack)是一个用来跟踪函数调用的内存结构。在程序执行时,每当一个函数被调用,它的信息会被压入栈中,形成了一个栈的结构。当函数执行完毕后,该函数的信息会从调用栈中被移除。

调用栈遵循"后进先出"(Last-In-First-Out,LIFO)的原则,即最后调用的函数首先执行和弹出。当一个函数被调用时,会将其执行上下文(Execution Context)压入调用栈,包含函数的局部变量、参数以及返回地址等信息。函数执行完毕后,对应的执行上下文会从调用栈中弹出,控制权回到调用该函数的位置继续执行。

调用栈在程序执行过程中起着重要的作用。它记录了函数调用的顺序,确保函数按照正确的顺序执行,并且可以正确地回溯到上一个函数的执行点。通过调用栈,我们可以追踪函数调用和退出的路径,帮助我们理解代码的执行流程,以及在调试时定位错误。

以下是一个简单的示例代码,展示了调用栈的工作原理:

function a() {
  console.log('a');
}

function b() {
  console.log('b');
  a();
}

function c() {
  console.log('c');
  b();
}

c();

在这个例子中,程序从调用c函数开始。执行上下文会被添加到调用栈的顶部。接着,c函数调用b函数,执行上下文被添加到调用栈的顶部。再接着,b函数调用a函数,执行上下文被添加到调用栈的顶部。当a函数执行完毕后,它的执行上下文从调用栈中被移除。然后是b函数执行完毕,最后是c函数执行完毕。最终,整个调用栈为空,程序执行结束。

需要注意的是,如果调用栈过深,超过了JavaScript引擎的限制,会导致"堆栈溢出"(Stack Overflow)错误,也就是我们所说的爆栈。这通常发生在递归函数中,当函数反复调用自身,而没有结束条件时,调用栈会不断增长,最终超出限制。在编写代码时,需要避免无限递归或者过深的函数嵌套,以避免堆栈溢出错误的发生。

简单来说,调用栈是用来管理函数调用关系的一种数据结构。

作用域链

作用域链(Scope Chain)是指在JavaScript中变量的查找过程。当代码中引用一个变量时,JavaScript引擎会按照预先设定的规则沿着作用域链查找该变量。

打个比方

假设我们有一个家庭,家庭成员分别是爷爷、爸爸和儿子。每个人都有自己的房间,房间中可以有各自的物品。 在这个比喻中,爷爷代表全局作用域,爸爸代表外部函数作用域,儿子代表内部函数作用域。房间中的物品就是变量和函数。

现在我们有一个任务,要找到儿子的玩具:

  1. 首先,我们在儿子的房间里查找,如果找到了玩具,就完成了任务。
  2. 如果在儿子的房间里没有找到玩具,那么我们会进入爸爸的房间,在爸爸的房间里查找。如果找到了玩具,就完成了任务。
  3. 如果在爸爸的房间里也没有找到玩具,那么我们会继续进入爷爷的房间,在爷爷的房间里查找。如果找到了玩具,就完成了任务。
  4. 如果连在爷爷的房间里都没有找到玩具,那么就表示任务失败,玩具不存在。

这个过程就类似于 JavaScript 的作用域链。当我们在代码中引用一个变量时,JavaScript 引擎会按照作用域链的顺序从内到外查找该变量的值。

以下是一个简单的示例代码,展示了作用域链的工作原理:

var a = 1; // 全局作用域

function outer() {
  var b = 2; // outer函数作用域

  function inner() {
    var c = 3; // inner函数作用域
    console.log(a + b + c); // 6
  }

  inner();
}

outer();

在这个例子中,程序从调用outer函数开始。当inner函数被调用时,JavaScript引擎会按照以下顺序查找变量c、b和a的值:

  1. 在inner函数的作用域中查找变量c,如果找到则使用该变量的值。
  2. 如果没有找到变量c,则继续向上查找,在outer函数的作用域中查找变量b,如果找到则使用该变量的值。
  3. 如果仍然没有找到变量b,则继续向上查找,在全局作用域中查找变量a,如果找到则使用该变量的值。
  4. 如果在全局作用域中也没有找到变量a,则会抛出一个"ReferenceError"错误。

需要注意的是,在JavaScript中,函数可以访问自己的作用域内的变量和函数,以及外部作用域的变量和函数。但是外部作用域无法访问函数内部的变量和函数。这种特性被称为"词法作用域"(Lexical Scope),也是作用域链的基础。

还有一个问题,在JavaScript中,我们需要通过函数所处的词法环境来确定该函数作用域的外层作用域,也就是该函数声明在哪,它所处的词法环境就是它的外层作用域,千万要注意,不是函数在哪调用,函数的外层作用域就在哪,而是函数在哪声明,外层作用域就在哪。

闭包

我们先看一下这个代码

var arr = []

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

for(var j = 0 ; j < arr.length ;j++) {
    arr[j]()
}

该代码首先定义了一个空数组 arr,然后使用一个 for 循环向其中添加了 10 个函数。

每个函数都有一个共同的功能:打印变量 i 的值。当我们在第二个 for 循环中遍历 arr 数组并调用每个函数时,它们会将变量 i 的值打印到控制台上。

然而,由于 JavaScript 中的变量作用域特性,这段代码的输出结果可能会让人感到困惑。

在循环中使用的变量 i 使用关键字 var 声明,它具有函数作用域而不是块级作用域。这意味着所有的函数共享同一个变量 i,并且在循环结束后,变量 i 的最终值是 10。

因此,在第二个 for 循环中调用每个函数时,它们都会输出变量 i 的最终值 10

这是因为当函数被调用时,它们访问的是共享的 i 变量,而不是在创建它们时的那个特定的 i 值。

所以,输出的结果将是连续的十个 10。

那么,我们想要让它输出0-9该怎么做呢?

1. let + {} 形成的块级作用域

要输出0-9,你需要让每个函数都能够访问到它对应的i值。由于 let 声明的变量具有块级作用域,所以可以在每次循环迭代中创建一个新的变量名,并将其赋值为当前的 i 值。

修改代码如下:

var arr = []

for(let i = 0 ; i<10 ; i++) {
    arr[i] = function (){
        console.log(i);
    }
}

for(var j = 0 ; j < arr.length ;j++) {
    arr[j]();
}

在这个版本的代码中,我们使用了 let 声明变量 i。由于每次迭代都会创建一个新的变量名 i,并将其赋值为当前的 i 值,所以每个函数都能够访问到它对应的i值,从而输出0-9。

2. 闭包

除了使用 let 声明变量之外,你还可以通过使用闭包来实现这个功能。闭包是指可以访问自己作用域之外或者父级作用域的变量的函数。你可以通过创建一个返回新函数的函数,来捕获每次循环中的 i 的值,并将其保存在闭包中。

下面是使用闭包的另一种方法:

var arr = []

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

for(var j = 0 ; j < arr.length ;j++) {
    arr[j]()
}

这段代码创建了一个空数组 arr。然后,使用一个 for 循环来添加函数到 arr 数组中。

在每次循环时,定义了一个内部函数 help(j),并立即调用它,并传入当前的变量 i 作为参数。help 函数的目的是为了帮助创建一个闭包,确保每个数组元素都有自己独立的作用域。

help 函数内部,定义了匿名函数 function(){console.log(j);},并将它赋值给 arr[j]。这个匿名函数是一个闭包,它能够捕获 help 函数中传入的参数 j 的值。

接下来,在第二个 for 循环中,遍历 arr 数组,并调用每个函数。由于每个函数都是一个闭包,它们捕获的是在 help 函数中传递的独立的 j 值,因此输出的结果将是 0 到 9 的序列。

这样修改后的代码使用了闭包来解决了原始版本中存在的问题。它确保每个函数都能够访问到正确的 j 值,而不是共享同一个变量 i 的最终值。因此,最终的输出结果将是期望的 0 到 9 的序列。

综上所述:在js中,根据词法作用域的规则,内部函数总是能访问外部函数中的变量,当通过调用一个外部函数返回一个内部函数后,即使外部函数已经执行完毕,但是内部函数引用了外部函数中的变量依然会保存在内存中,这些外部函数中的变量形成的集合叫闭包

3.闭包的优缺点

  • 优点:实现私有化变量

  • 缺点:内存泄漏(调用栈可用空间变小了)

总的来说,闭包是 JavaScript 中非常有用的特性,能够帮助我们解决一些编程问题,但需要注意内存管理方面的风险。在实际开发中,需要根据具体情况合理地应用闭包,以实现代码的简洁和可维护性,同时避免潜在的内存泄漏问题。