26-闭包

4,521次阅读
没有评论

共计 5700 个字符,预计需要花费 15 分钟才能阅读完成。

闭包的引入

我们知道,变量根据作用域的不同分为两种:全局变量和局部变量。

  • 函数内部可以访问全局变量和局部变量。

  • 函数外部只能访问全局变量,不能访问局部变量。

  • 当函数执行完毕,本作用域内的局部变量会销毁。

比如下面这样的代码:

function foo() {let a = 1;}

foo();
console.log(a); // 打印报错:Uncaught ReferenceError: a is not defined

上方代码中,由于变量 a 是函数内的局部变量,所以外部无法访问。

但是,在有些场景下,我们就是想要在函数外部访问 函数内部作用域的局部变量,那要怎么办呢?这就引入了闭包的概念。

什么是闭包

闭包(closure)的概念

闭包 :如果 外部作用域 有权访问另外一个 函数内部 局部变量 时,那就产生了闭包。这个内部函数称之为闭包函数。注意,这里强调的是访问 局部变量

闭包代码举例:

function fun1() {
  const a = 10;
  return function fun2() {console.log(a);
  };
}
fun1();
// 调用外部函数,就能得到内部函数,并用 变量 result 接收
const result = fun1();
// 在 fun1 函数的外部,执行了内部函数 fun2,并访问到了 fun2 的内部变量 a
result(); // 10

打印结果:

10

上方代码中,外部作用域(即全局作用域)访问了函数 fun1 中的局部变量,那么,在 fun1 中就产生了闭包,函数 fun1 是闭包函数。

全局作用域中,并没有定义变量 a。正常情况下作为函数内的局部变量 a,无法被外部访问到。但是通过闭包,我们最后还是可以在全局作用域中拿到局部变量 a 的值。

注意,闭包函数是 fun1,不是 fun2。fun2 在这里的作用是让全局作用域访问到变量 a,fun2 只是一个桥梁。

闭包的生命周期

  1. 产生:内部函数 fn1 被声明时(即被创建时,不是被调用时)就产生了。

  2. 死亡:嵌套的内部函数成为垃圾对象时。(比如 fun1 = null,就可以让 fun1 成为垃圾对象)

在 chrome 浏览器控制台中,调试闭包

上面的代码中,要怎么验证,确实产生了闭包呢?我们可以在 chrome 浏览器的控制台中设置断点,当代码执行到 console.log(a)这一行的时候,如下图所示:

26- 闭包

上图中,Local 指的是局部作用域,Global 指的是全局作用域;而 Closure 则是 闭包,fn1 是一个闭包函数。

闭包的表现形式

形式 1:将一个函数作为另一个函数的返回值

    function fn1() {
      var a = 2

      function fn2() {
        a++
        console.log(a)
      }
      return fn2
    }

    var f = fn1();   // 执行外部函数 fn1,返回的是内部函数 fn2
    f() // 3       // 执行 fn2
    f() // 4       // 再次执行 fn2

当 f()第二次执行的时候,a 加 1 了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量 a 就消失了。

上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。

也就是说,要看闭包对象创建了几个,就看:外部函数执行了几次(与内部函数执行几次无关)。

形式 2:将函数作为实参传递给另一个函数调用

在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

    function showDelay(msg, time) {setTimeout(function() {  // 这个 function 是闭包,因为是嵌套的子函数,而且引用了外部函数的变量 msg
        alert(msg)
      }, time)
    }
    showDelay('qianguyihao', 2000)

上面的代码中,闭包是里面的 function,因为它是嵌套的子函数,而且引用了外部函数的变量 msg。

闭包的作用

  • 作用 1:延长局部变量的生命周期。

  • 作用 2:让函数外部可以操作(读写)函数内部的数据(变量 / 函数)。

代码演示:

function fun1() {
  let a = 2

  function fun2() {
    a++
    console.log(a)
  }
  return fun2;
}

const foo = fun1();   // 执行外部函数 fn1,返回的是内部函数 fn2
foo() // 3       // 执行 fun2
foo() // 4       // 再次执行 fun2

上方代码中,foo 代表的就是整个 fun2 函数。当执行了 foo() 语句之后,也就执行了 fun2()函数,fun1() 函数内就产生了闭包。

作用 1 分析

一般来说,在 fn1() 函数执行完毕后,它里面的变量 a 会立即销毁。但此时由于产生了闭包,所以 fun1 函数中的变量 a 不会立即销毁,仍然保留在内存中,因为 fn2 函数还要继续调用变量 a。只有等所有函数把变量 a 调用完了,变量 a 才会销毁。

作用 2 分析:

在执行 foo()语句之后,竟然能够打印出 3,这就完美通过闭包实现了:全局作用域成功访问到了局部作用域中的变量 a。

达到的效果是:外界看不到变量 a,但可以操作 a 。当然,如果你真想看到 a,可以在 fun2 中将 a 返回即可。

闭包的应用场景

场景 1:高阶函数

题目:不同的班级有不同成绩检测标准。比如:A 班的合格线是 60 分,B 班的合格线是 70 分。已知某个人班级和分数,请用闭包函数判断他的成绩是否合格。

思路:创建成绩检测函数 checkStandard(n),检查成绩 n 是否合格,函数返回布尔值。

代码实现:

// 高阶函数:判断学生的分数是否合格。形参 standardTemp 为标准线
function createCheckTemp(standardTemp) {
  // 形参 n 表示具体学生的分数
  function checkTemp(n) {if (n >= standardTemp) {alert('成绩合格');
    } else {alert('成绩不合格');
    }
  }
  return checkTemp;
}

// 创建一个 checkStandard_A 函数,它以 60 分为合格线
var checkStandard_A = createCheckTemp(60);
// 再创建一个 checkStandard_B 函数,它以 70 分为合格线
var checkStandard_B = createCheckTemp(70);

// 调用函数
checkStandard_A(65); // 成绩合格
checkStandard_B(65); // 成绩不合格

对于 A 班来说,它的闭包函数是 createCheckTemp(),闭包范围是 checkTemp()函数和参数 standardTemp = 60。对于 B 班来说,它的闭包函数是全新的 createCheckTemp(),闭包范围是全新的 checkTemp() 函数和全新的参数standardTemp = 70

因为有闭包存在,所以,并不会因为 createCheckTemp() 执行完毕后就销毁 standardTemp 的值;且 A 班和 B 班的 standardTemp 参数不会混淆。

备注:关于“高阶函数”的更多解释,我们在以后的内容中讲解。

场景 2:封装 JS 模块

闭包的第二个使用场景是:定义具有特定功能的 JS 模块,将所有的数据和功能都封装在一个函数内部,只向外暴露指定的对象或方法。模块的调用者,只能调用模块暴露的对象或方法来实现对应的功能。

比如有这样一个需求:定义一个私有变量 a,要求 a 只能被进行指定操作(加减),不能进行其他操作(乘除)。在 Java、C++ 等语言中,有私有属性的概念,但在 JS 中只能通过闭包模拟。

我们来看看下面的代码,如何通过闭包封装 JS 模块。

写法 1:

(1)myModule.js:(定义一个模块,向外暴露多个方法,供外界调用)

function myModule() {
    // 私有数据
    var msg = 'Qinguyihao Haha'

    // 操作私有数据的函数
    function doSomething() {console.log('doSomething()' + msg.toUpperCase()); // 字符串大写
    }

    function doOtherthing() {console.log('doOtherthing()' + msg.toLowerCase()) // 字符串小写
    }

    // 通过【对象字面量】的形式进行包裹,向外暴露多个函数
    return {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
}

上方代码中,外界只能通过 doSomething1 和 doOtherthing2 来操作里面的数据,但不让外界看到里面的具体实现。

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title> 闭包的应用 - 自定义 JS 模块 </title>
</head>
<body>
<!--
闭包应用举例: 定义 JS 模块
  * 具有特定功能的 js 文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  *【重要】只向外暴露一个包含 n 个方法的对象或方法
  * 模块的使用者, 只需要调用模块暴露的对象或者方法来实现对应的功能
-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
    var module = myModule();
    module.doSomething1();
    module.doOtherthing2();
</script>
</body>
</html>

写法 2:

同样是实现上面的功能,我们还采取另外一种写法,写起来更方便。如下:

(1)myModule2.js:(是一个立即执行的匿名函数)

(function () {
    // 私有数据
    var msg = 'Qinguyihao Haha'

    // 操作私有数据的函数
    function doSomething() {console.log('doSomething()' + msg.toUpperCase())
    }

    function doOtherthing() {console.log('doOtherthing()' + msg.toLowerCase())
    }

    // 外部函数是即使运行的匿名函数,我们可以把两个方法直接传给 window 对象
    window.myModule = {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
})()

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title> 闭包的应用 - 自定义 JS 模块 </title>
</head>
<body>
<!--
闭包的应用 2 : 定义 JS 模块
  * 具有特定功能的 js 文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  * 只向外暴露一个包信 n 个方法的对象或函数
  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->

<!-- 引入 myModule 文件 -->
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
    myModule.doSomething1()
    myModule.doOtherthing2()
</script>
</body>
</html>

上方两个文件中,我们在 myModule2.js 里直接把两个方法直接传递给 window 对象了。于是,在 index.html 中引入这个 js 文件后,会立即执行里面的匿名函数。在 index.html 中把 myModule 直接拿来用即可。

小结:

写法 1 和写法 2 都采用了闭包。

内存溢出和内存泄露

我们再讲两个概念,这两个概念和闭包有些联系。

内存泄漏

内存泄漏 占用的内存 没有及时释放。

内存泄露的次数积累多了,就容易导致内存溢出。

常见的内存泄露

1、意外的全局变量

2、没有及时清理的计时器或回调函数

3、闭包

情况 1 举例:

// 意外的全局变量
function fn() {a = new Array(10000000);
  console.log(a);
}

fn();

情况 2 举例:

// 没有及时清理的计时器或回调函数
var intervalId = setInterval(function () { // 启动循环定时器后不清理
  console.log('----')
}, 1000)

// clearInterval(intervalId);  // 清理定时器

情况 3 举例:

function fn1() {
  var a = 4;
  function fn2() {console.log(++a)
  }
  return fn2
}
var f = fn1()
f()

// f = null // 让内部函数成为垃圾对象 --> 回收闭包

内存溢出

内存溢出 :程序运行时出现的错误。当程序运行 需要的内存 超过 剩余的内存 时,就抛出内存溢出的错误。

代码举例:

    var obj = {};
    for (var i = 0; i < 10000; i++) {obj[i] = new Array(10000000);  // 把所有的数组内容都放到 obj 里保存,导致 obj 占用了很大的内存空间
    console.log("-----");
    }

闭包是否会造成内存泄漏

一般来说,答案是否定的。因为内存泄漏是非预期情况,本来想回收,但实际没回收;而闭包是预期情况,一般不会造成内存泄漏。

但如果因代码质量不高,滥用闭包,也会造成内存泄漏。

闭包面试题

代码举例:

function addCount() {
  let count = 0;
  return function () {
    count = count + 1;
    console.log(count);
  };
}

const fun1 = addCount();
const fun2 = addCount();
fun1();
fun2();

fun1();
fun2();

打印结果:

1
1
2
2

代码解释:

(1)fun1 和 fun2 这两个闭包函数是互不影响的,因此第一次调用时,count 变量都是 0,最终各自都输出 1。

(2)第二次调用时,由于闭包有记忆性,所以各自会在上一次的结果上再加 1,因此输出 2。

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-09-10发表,共计5700字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)