浅谈JavaScript闭包,小白的JS学习之路!

前言

在JavaScript中,闭包是一种强大而灵活的特性,它不仅允许变量私有化,而且提供了一种在函数执行完毕后仍然保持对外部作用域变量引用的机制。本文将深入讨论JavaScript闭包的概念、优点、缺点以及如何避免潜在的内存泄漏问题。

调用栈与作用域链

在理解闭包之前,首先需要了解调用栈和作用域链的概念。

调用栈

调用栈是用来管理函数调用关系的数据结构。当一个函数执行时,会将其执行上下文推入调用栈,如下图所示:

浅谈JavaScript闭包,小白的JS学习之路!_第1张图片

浅谈JavaScript闭包,小白的JS学习之路!_第2张图片

浅谈JavaScript闭包,小白的JS学习之路!_第3张图片

当函数执行完毕后,它的执行上下文就会从调用栈中弹出,如下图:

浅谈JavaScript闭包,小白的JS学习之路!_第4张图片

作用域链

作用域链是通过词法作用域(静态作用域)来确定某个作用域的外层作用域,在查找变量时,会按照由内而外的链状关系进行查找。这种链状关系叫做作用域链

  • 对于使用 var 声明的变量,它们位于变量环境。
  • 对于使用 letconst 声明的变量,它们位于词法环境。
  • outer属性指向外层作用域,全局执行上下文的outer指向null
  • outer的值取决于函数声明在何处而非在何处调用

如下图:

浅谈JavaScript闭包,小白的JS学习之路!_第5张图片

从bar的执行上下文到全局执行上下文以及foo的执行上下文到全局上下文的这种查找的链状关系,就叫做作用域链。

闭包的概念

闭包是指能够访问其外部函数中声明的变量的函数,即使外部函数执行完毕。在JavaScript中,由于词法作用域的存在,内部函数总是可以访问外部函数中声明的变量。我们来看下一个例子:

function foo() {
    var myName ='旭旭'
    let test1 = 1 
    let 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());

在上面的例子中,foo函数在执行完毕后,产生了一个闭包,内容为myName = '旭旭'test = 1,当foo()执行完成后,垃圾回收机制将foo的执行上下文清理掉了,但是由于,foo函数中的innerBar对象中的,getName函数以及setName函数中存在对test1myName的引用,所以在垃圾回收机制执行后,留下了myName = '旭旭'test = 1,他们的集合称作闭包。即,下图黄框部分:

浅谈JavaScript闭包,小白的JS学习之路!_第6张图片

闭包的简单应用

我们先来看这样一段代码:

 
  
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]()
}

代码看上去,像是要完成输出0-9的功能,但是实际上的输出结果为1010,因为在for循环声明的i是由var声明的,var存在声明提升,所以相当于在全局声明的i,而当第一个for循环结束后,i达到了10,并且声明了10个函数体,存到了数组arr[]中,在第二次的for循环中,将arr10个函数体取出并且调用,调用结果为打印i,而此时的i10,所以会输出1010

浅谈JavaScript闭包,小白的JS学习之路!_第7张图片

如果我们要在改动最小的情况下,使它的功能变为打印0-9那么我们可以将第一个for循环中的i,改为使用let声明

浅谈JavaScript闭包,小白的JS学习之路!_第8张图片

因为使用let声明i的时候,每次执行for循环都会形成一个块级作用域,而在执行输出i的语句时,我们会首先在这个形成的块级作用域查找,从而完成每个作用域中的i保留为0-9的值,所以在输出时能够实现输出0-9

但是如果我们的第一个for循环仍要使用var声明i,那么我们就可以利用闭包来实现输出0-9的功能,代码如下:

 
  
var arr = []
for (var i = 0; i < 10; i++) {

  (arr[i] = function (j) {
    console.log(j)

  })(i)
}

浅谈JavaScript闭包,小白的JS学习之路!_第9张图片

在这个过程中我们直接在创建函数的时候,直接对其调用,从而利用闭包的把i此时的值留住,形成闭包,闭包中的内容为arr[i]arr[i]中的内容为function(j){console.log(j)}ji,所以输出0-9

闭包的优点

变量私有化

闭包允许在内部函数中访问外部函数的变量,从而实现变量的私有化。这种机制在框架级别的开发以及一些设计模式中非常有用,可以避免变量被外部随意修改。

 
  
function counter() {
  let count = 0;

  return function() {
    count++;
    console.log(count);
  };
}

const increment = counter();
increment(); // 输出 1
increment(); // 输出 2

在上面的例子中,count 变量被私有化在 counter 函数内部,外部无法直接访问或修改它。

闭包的缺点

内存泄漏

闭包的一个潜在问题是内存泄漏。由于闭包使得内部函数保持对外部函数作用域中变量的引用,如果这些引用没有被及时释放,可能导致内存占用过高。

 
  
function createHeavyObject() {
  const heavyObject = /* 创建一个占用大量内存的对象 */;

  return function() {
    console.log(heavyObject);
  };
}

const myClosure = createHeavyObject();
// 此时myClosure包含对createHeavyObject函数作用域中heavyObject的引用

在上述例子中,myClosure 包含对 createHeavyObject 函数作用域中 heavyObject 的引用,即使外部不再需要 heavyObject,它依然无法被垃圾回收。要避免内存泄漏,可以手动解除对不再需要的引用,或者使用一些优化手段。

如何避免内存泄漏

为了避免闭包导致的内存泄漏,可以采取以下措施:

1. 及时解除引用

当不再需要闭包时,手动将对外部作用域变量的引用解除,让垃圾回收机制能够回收相关资源。

 
  
function createHeavyObject() {
  const heavyObject = /* 创建一个占用大量内存的对象 */;

  return function() {
    console.log(heavyObject);
  };
}

const myClosure = createHeavyObject();
// 手动解除引用
myClosure = null;

2. 使用垃圾回收优化

一些现代 JavaScript 引擎会对闭包进行优化,自动检测不再需要的引用并进行回收。但这并不是一劳永逸的解决方案,仍然建议在代码中注意及时释放不再需要的引用。

结语

闭包是JavaScript中强大而灵活的特性,能够提供变量私有化的能力。然而,要小心使用闭包,以防止潜在的内存泄漏问题。及时释放不再需要的引用是保持代码健康的重要步骤,合理利用闭包将为你的代码带来便利和安全性。

如果有疑问或者错误,欢迎在评论区指出!

浅谈JavaScript闭包,小白的JS学习之路!_第10张图片


原文链接:https://juejin.cn/post/7300779577059131402
 

你可能感兴趣的:(javascript,学习,开发语言)