学习闭包前提先了解一下作用域的概念:https://www.yuque.com/linhe-8mnf5/fxyxkm/eou35k

闭包

1.什么是闭包

函数执行后返回结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,即形成了闭包。

简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。

通俗的讲:闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者直接说闭包是个内嵌函数也可以。

闭包产生的本质就是:当前环境中存在指向父级作用域的引用

在 JavaScript 中,根据词法作用域的 规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个 内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包

2.闭包原理

函数执行分成两个阶段(预编译阶段和执行阶段)。

  • 在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,如果已存在“闭包”,则只需要增加对应属性值即可。
  • 执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量

利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被销毁后才被销毁。

3.闭包优点

  • 可以从内部函数访问外部函数的作用域中的变量,且访问到的变量长期驻扎在内存中,可供之后使用
  • 避免变量污染全局
  • 把变量存到独立的作用域,作为私有成员存在

4.闭包缺点

  • 对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会导致内存泄漏
  • 对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域链长度
  • 可能获取到意外的值(captured value)
  • 闭包会导致内存占用过高,因为变量都没有释放内存

5.闭包的使用场景

  • 函数作为返回值
  • 函数作为参数传递
  • 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
  • IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量
  1. // 函数作为返回值
  2. function F1(){
  3. var a = 100;
  4. return function(){
  5. console.log(a) //自由变量 定义时候查找 a=100
  6. }
  7. }
  8. var f1 = F1()
  9. var a = 200
  10. f1()
  11. // 另一种表现形式
  12. var fun3;
  13. function fun1() {
  14. var a = 2
  15. fun3 = function() {
  16. console.log(a);
  17. }
  18. }
  19. fun1();
  20. fun3();
  1. // 函数作为参数
  2. function print (fn) {
  3. let a = 200
  4. fn()
  5. }
  6. let a= 100
  7. function fn () {
  8. console.log(a)
  9. }
  10. print(fn)
  1. // 定时器
  2. setTimeout(function handler(){
  3. console.log('1');
  4. },1000);
  5. // 事件监听
  6. $('#app').click(function(){
  7. console.log('Event Listener');
  8. });
  1. // 立即执行函数
  2. var a = 2;
  3. (function IIFE(){
  4. console.log(a); // 输出2
  5. })();

6.典型题

  1. for (var i = 0; i < 4; i++) {
  2. setTimeout(function() {
  3. console.log(i);
  4. }, 300);
  5. }

上边打印出来的都是 4, 可能部分人会认为打印的是 0,1,2,3

原因:js 执行的时候首先会先执行主线程,异步相关的会存到异步队列里,当主线程执行完毕开始执行异步队列, 主线程执行完毕后,此时 i 的值为 4,说以在执行异步队列的时候,打印出来的都是 4(这里需要大家对 event loop 有所了解(js 的事件循环机制))

setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。

因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 5 了,因此最后输出的连续就都是 5。

解决方法

  1. //方法一:
  2. for (var i = 0; i < 4; i++) {
  3. setTimeout(
  4. (function(i) {
  5. return function() {
  6. console.log(i);
  7. };
  8. })(i),
  9. 300
  10. );
  11. }
  12. // 或者
  13. for (var i = 0; i < 4; i++) {
  14. setTimeout(
  15. (function() {
  16. var temp = i;
  17. return function() {
  18. console.log(temp);
  19. };
  20. })(),
  21. 300
  22. );
  23. }
  24. //这个是通过自执行函数返回一个函数,然后在调用返回的函数去获取自执行函数内部的变量,此为闭包
  25. //方法二:
  26. for (var i = 0; i < 4; i++) {
  27. (function(i) {
  28. setTimeout(function() {
  29. console.log(i);
  30. }, 300);
  31. })(i);
  32. }
  33. // 大部分都认为方法一和方法二都是闭包,我认为方法一是闭包,而方法二是通过创建一个自执行函数,使变量存在这个自执行函数的作用域里