文章目录

  • 闭包
  • 一、什么是闭包
  • 二、产生闭包的条件
  • 三、闭包变量存储的位置
  • JS 堆栈内存释放
  • 四、常见的闭包
  • 闭包1:将一个函数作为另一个函数的返回值
  • 闭包2. 将函数作为实参传递给另一个函数调用
  • 闭包3.函数作为参数
  • 闭包4. IIFE(自执行函数)
  • 闭包5. 循环赋值
  • 闭包6.节流防抖
  • 闭包7.函数柯里化
  • 五、闭包的作用
  • 六、闭包的生命周期
  • 闭包何时被销毁?-垃圾回收机制
  • 七、闭包的缺点及解决
  • 八、内存溢出和内存泄露
  • 内存溢出
  • 内存泄漏
  • 九、经典面试题


闭包

提问:

  1. 什么是闭包?
  2. 闭包有哪些实际运用场景?
  3. 闭包是如何产生的?
  4. 闭包产生的变量如何被回收?

一、什么是闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。

二、产生闭包的条件

1.函数嵌套

2.内部函数引用了外部函数的数据(变量/函数)。

PS:还有一个条件是外部函数被调用,内部函数被声明。比如:

function fn1() {
        var a = 2
        var b = 'abc'

        function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数)
            console.log(a)
        }

    }

    fn1();

    function fn3() {
        var a = 3
        var fun4 = function () {  //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前
            console.log(a)
        }
    }

    fn3();

三、闭包变量存储的位置

直接说明:闭包中的变量存储的位置是堆内存。

  • 假如闭包中的变量存储在栈内存中,那么栈的回收 会把处于栈顶的变量自动回收。所以闭包中的变量如果处于栈中那么变量被销毁后,闭包中的变量就没有了。所以闭包引用的变量是出于堆内存中的。
JS 堆栈内存释放
  • 堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串。
  • 堆内存释放:将引用类型的空间地址变量赋值成 null,或没有变量占用堆内存了浏览器就会释放掉这个地址
  • 栈内存:提供代码执行的环境和存储基本类型值。
  • 栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。

但栈内存的释放也有特殊情况:① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。② 全局下的栈内存只有页面被关闭的时候才会被释放

四、常见的闭包

  • 将一个函数作为另一个函数的返回值
  • 将函数作为实参传递给另一个函数调用。
闭包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. 将函数作为实参传递给另一个函数调用

使用回调函数就是在使用闭包

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

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

闭包3.函数作为参数
var a = 'cba'
function foo(){
    var a = 'foo'
    function fo(){
        console.log(a)
    }
    return fo
}

function f(p){
    var a = 'f'
    p()
}
f(foo())
/* 输出
*   foo
/

使用 return fo 返回回来,fo() 就是闭包,f(foo()) 执行的参数就是函数 fo,因为 fo() 中的 a 的上级作用域就是函数foo(),所以输出就是foo

闭包4. IIFE(自执行函数)
var n = 'cba';
(function p(){
    console.log(n)
})()
/* 输出
*   cba
/

同样也是产生了闭包p(),存在 window下的引用 n

闭包5. 循环赋值
for(var i = 0; i<10; i++){
  (function(j){
       setTimeout(function(){
        console.log(j)
    }, 1000) 
  })(i)
}
闭包6.节流防抖
// 防抖
function debounce(fn, delay = 300) {
  //默认300毫秒
  let timer;
  return function () {
    const args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args); // 改变this指向为调用debounce所指的对象
    }, delay);
  };
}

window.addEventListener(
  "scroll",
  debounce(() => {
    console.log(111);
  }, 1000)
);

// 节流
// 设置一个标志
function throttle(fn, delay) {
  let flag = true;
  return () => {
    if (!flag) return;
    flag = false;
    timer = setTimeout(() => {
      fn();
      flag = true;
    }, delay);
  };
}

window.addEventListener(
  "scroll",
  throttle(() => {
    console.log(111);
  }, 1000)
);
闭包7.函数柯里化
function currying(fn, ...args) {
  const length = fn.length;
  let allArgs = [...args];
  const res = (...newArgs) => {
    allArgs = [...allArgs, ...newArgs];
    if (allArgs.length === length) {
      return fn(...allArgs);
    } else {
      return res;
    }
  };
  return res;
}

// 用法如下:
// const add = (a, b, c) => a + b + c;
// const a = currying(add, 1);
// console.log(a(2,3))

五、闭包的作用

  • 作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
  • 作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

六、闭包的生命周期

  1. 产生: 嵌套内部函数fn2被声明时就产生了(不是在调用)
  2. 死亡: 嵌套的内部函数成为垃圾对象时。(比如f = null,就可以让f成为垃圾对象。意思是,此时f不再引用闭包这个对象了
闭包何时被销毁?-垃圾回收机制

什么时候闭包被销毁?

最终结论-如果没有特殊的垃圾回收算法(暂时没有搜索到有这种算法)会造成闭包常驻!除非手动设置为null 否则就会造成内存泄露!

七、闭包的缺点及解决

缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。

解决:能不用闭包就不用,及时释放。比如:

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

总而言之,你需要它,就是优点;你不需要它,就成了缺点。

八、内存溢出和内存泄露

内存溢出

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

代码举例:

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

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

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

常见的内存泄露

  • 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举例:

<script type="text/javascript">
  function fn1() {
    var a = 4;
    function fn2() {
      console.log(++a)
    }
    return fn2
  }
  var f = fn1()
  f()

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

九、经典面试题

for(var i = 0;i < 5;i++) {
  setTimeout(function() {
    console.log(i++);
  },4000)
}
console.log(i)

因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 5 了,所以会输出一堆 5。

  • setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
  • 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 5 了,因此最后输出的连续就都是 5。

解决办法

  • 第一种是使用闭包的方式

相当于使用了立即执行函数(IIFE)

当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行

for(var i=0;i<5;i++){
  (function(x) {
    setTimeout(function() {
      console.log(x++)
    },4000)
  })(i);
}

解释:

首先创建全局执行上下文,进入for循环,第一轮循环需要执行立即执行函数,于是创建立即执行函数的上下文,这里i=0,那么x=0,因为是异步,浏览器将处理的结果放入任务队列,也就是将数值0放入任务队列,此时立即执行函数的上下文从栈中弹出,回到全局执行上下文,进入第二次for循环,此时i=1,需要执行立即执行函数,创建立即执行函数的上下文,将数值1放入任务队列,立即执行函数的上下文从栈中弹出,后面的三次循环一样。

当全局的代码执行完毕后,执行异步代码,此时将任务队列里面的数值依次输出。

  • 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i < 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    4000,
    i
  )
}
  • 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <5 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, 4000)
}