js--闭包与垃圾回收机制

前言

  闭包和垃圾回收机制常常作为前端学习开发中的难点,也经常在面试中遇到这样的问题,本文记录一下在学习工作中关于这方面的笔记。

正文

 1.闭包

  闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。作为一个JavaScript开发者,理解闭包十分重要。

  1.1闭包是什么?

  闭包就是一个函数引用另一个函数的变量,内部函数被返回到外部并保存时产生,(内部函数的作用域链AO使用了外层函数的AO)

       因为变量被引用着所以不会被回收,因此可以用来封装一个私有变量,但是不必要的闭包只会增加内存消耗。

  闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。或者说闭包就是子函数可以使用父函数的局部变量,还有父函数的参数。

  1.2闭包的特性

   ①函数嵌套函数

  ②函数内部可以引用函数外部的参数和变量

  ③参数和变量不会被垃圾回收机制回收

  1.3理解闭包

  基于我们所熟悉的作用域链相关知识,我们来看下关于计数器的问题,如何实现一个函数,每次调用该函数时候计数器加一。

    var counter=0;
    function demo3(){
        console.log(counter+=1);       
    }
    demo3();//1
    demo3();//2
    var counter=5;
    demo3();  //6
  上面的方法,如果在任何一个地方改变counter的值 计数器都会失效,javascript解决这种问题用到闭包,就是函数内部内嵌函数,再来看下利用闭包如何实现。
     function add() {
            var counter = 0;
            return function plus() {
                counter += 1;
                return counter
            }      
        }
        var count=add()// 确保之后调用的是一个add方法
        console.log(count())//1
        var counter=100
        console.log(count())//2

  上面就是一个闭包使用的实例 ,函数add内部内嵌一个plus函数,count变量引用该返回的函数,每次外部函数add执行的时候都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址,把plus函数嵌套在add函数内部,这样就产生了counter这个局部变量,每次调用count函数,该局部变量值加一,从而实现了真正的计数器问题。

  1.4闭包的主要实现形式

  这里主要通过两种形式来学习闭包:

  ①函数作为返回值,也就是上面的例子中用到的。

        function showName(){
                var name="xiaoming"
                return function(){
                    return name
                }
            }
            var name1=showName()
            console.log(name1())    

  闭包就是能够读取其他函数内部变量的函数。闭包就是将函数内部和函数外部连接起来的一座桥梁。

  ②函数作为参数传递

        var num = 15
            var foo = function(x){
                if(x>num){
                    console.log(x)
                }  
            }
            function foo2(fnc){
                var num=30
                fnc(25)
            }
            foo2(foo)//25

  上面这段代码中,函数foo作为参数传入到函数foo2中,在执行foo2的时候,25作为参数传入foo中,这时判断的x>num的num取值是创建函数的作用域中的num,即全局的num,而不是foo2内部的num,因此打印出了25。

  1.5闭包的优缺点

  优点:

  ①保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突

  ②在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)

  ③匿名自执行函数可以减少内存消耗

  缺点:

  ①其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;

  ②其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响。

  1.6闭包的使用

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

  console.log(i);

  我们来看上面的问题,这是一道很常见的题,可这道题会输出什么,一般人都知道输出结果是 5,5,5,5,5,5,你仔细观察可能会发现这道题还有很多巧妙之处,这6个5的输出顺序具体是怎样的?5 -> 5,5,5,5,5 ,了解同步异步的人也不难理解这种情况,基于上面的问题,接下来思考如何实现5 -> 0,1,2,3,4这样的顺序输出呢?

    for (var i = 0; i < 5; i++) {
        (function(j) {  // j = i
            setTimeout(function() {
                console.log( j);
            }, 1000);
        })(i);
    }
    console.log( i);
//5 -> 0,1,2,3,4    

  这样在for循环种加入匿名函数,匿名函数入参是每次的i的值,在同步函数输出5的一秒之后,继续输出01234。

    for (var i = 0; i < 5; i++) {
        setTimeout(function(j) {
            console.log(j);
        }, 1000, i);
    }
    console.log( i);
    //5 -> 0,1,2,3,4

  仔细查看setTimeout的api你会发现它还有第三个参数,这样就省去了通过匿名函数传入i的问题。

  var output = function (i) {
      setTimeout(function() {
          console.log(i);
      }, 1000);
  };

  for (var i = 0; i < 5; i++) {
      output(i);  // 这里传过去的 i 值被复制了
  }

  console.log(i);
  //5 -> 0,1,2,3,4

  这里就是利用闭包将函数表达式作为参数传递到for循环中,同样实现了上述效果。

  for (let i = 0; i < 5; i++) {
      setTimeout(function() {
          console.log(new Date, i);
      }, 1000);
  }
  console.log(new Date, i);
  //5 -> 0,1,2,3,4

  知道let块级作用域的人会想到上面的方法。但是如果要实现0 -> 1 -> 2 -> 3 -> 4 -> 5这样的效果呢。

  for (var i = 0; i < 5; i++) {
      (function(j) {
          setTimeout(function() {
              console.log(new Date, j);
          }, 1000 * j);  // 这里修改 0~4 的定时器时间
      })(i);
  }

  setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
      console.log(new Date, i);
  }, 1000 * i);
  //0 -> 1 -> 2 -> 3 -> 4 -> 5

  还有下面的代码,通过promise来实现。

  const tasks = [];
  for (var i = 0; i < 5; i++) {   // 这里 i 的声明不能改成 let,如果要改该怎么做?
      ((j) => {
          tasks.push(new Promise((resolve) => {
              setTimeout(() => {
                  console.log(new Date, j);
                  resolve();  // 这里一定要 resolve,否则代码不会按预期 work
              }, 1000 * j);   // 定时器的超时时间逐步增加
          }));
      })(i);
  }

  Promise.all(tasks).then(() => {
      setTimeout(() => {
          console.log(new Date, i);
      }, 1000);   // 注意这里只需要把超时设置为 1 秒
  });
  //0 -> 1 -> 2 -> 3 -> 4 -> 5
  const tasks = []; // 这里存放异步操作的 Promise
  const output = (i) => new Promise((resolve) => {
      setTimeout(() => {
          console.log(new Date, i);
          resolve();
      }, 1000 * i);
  });

  // 生成全部的异步操作
  for (var i = 0; i < 5; i++) {
      tasks.push(output(i));
  }

  // 异步操作完成之后,输出最后的 i
  Promise.all(tasks).then(() => {
      setTimeout(() => {
          console.log(new Date, i);
      }, 1000);
  });
  //0 -> 1 -> 2 -> 3 -> 4 -> 5
// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  // 声明即执行的 async 函数表达式
    for (var i = 0; i < 5; i++) {
        if (i > 0) {
            await sleep(1000);
        }
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();
//0 -> 1 -> 2 -> 3 -> 4 -> 5

  上面的代码中都用到了闭包,总之,闭包找到的是同一地址中父级函数中对应变量最终的值。

  2.垃圾回收机制

  JavaScript 中的内存管理是自动执行的,而且是不可见的。我们创建基本类型、对象、函数……所有这些都需要内存。下面是 JavaScript 垃圾回收机制的详细过程

 

  1. JavaScript 引擎会定期进行垃圾回收,以检查哪些对象不再被使用,并将其回收。

  2. 在进行垃圾回收之前,JavaScript 引擎会建立一张对象图,用来表示当前存在的所有对象之间的引用关系。

  3. 对象图中的每个对象都有一个状态,表示该对象是否被使用。如果对象被使用,则其状态为“活动”;如果对象不再被使用,则其状态为“非活动”。

  4. 在建立对象图时,JavaScript 引擎会从“根”对象开始,递归地搜索所有的对象。根对象是指全局对象和当前执行上下文的对象,它们是所有对象的祖先。

  5. 对于每个被搜索到的对象,JavaScript 引擎会将其状态设置为“活动”。

  6. 当 JavaScript 引擎完成对象图的搜索后,它会找出所有状态为“非活动”的对象,并将这些对象的内存回收。

  7. JavaScript 引擎可以使用多种不同的垃圾回收算法,例如标记-清除算法和引用计数算法。

 

  通常用采用的垃圾回收有两种方法:标记清除、引用计数。

  1、标记清除

  (1) 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。

  (2) 而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。

  (3) 最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间

  优点:
  a、可以回收循环引用的对象空间。相对于引用计数算法来说:解决对象循环引用的不能回收问题。

  缺点:
  a、容易产生碎片化空间,浪费空间、不能立即回收垃圾对象。

  b、空间碎片化:所谓空间碎片化就是由于当前所回收的垃圾对象,在地址上面是不连续的,由于这种不连续造成了我们在回收之后分散在各个角落,造成后续使用的问题

  2.引用计数

  引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。

  相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,

  则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,

  它就会释放那些引用次数为0的值所占的内存。

  优点:
  a、可以即时回收垃圾对象、减少程序卡顿时间。发现垃圾立即回收(因为根据当前的引用数值是否为0判断他是否是一个垃圾,如果是垃圾就回收内存空间,释放内存)

  b、最大限度的减少程序暂停(应用程序在执行的过程中必定会对内存进行消耗,而当前的执行平台内存空间是有上限的,所以内存肯定会有占满的时候。由于引用计数算法时刻监控着那些引用数值为0的对象,当内存爆满的时候会去找那些引用数值为0的对象释放其内存,这个也就保证了当前的内存空间不会有占满的时候)
  缺点:
  a、无法回收循环引用的对象、资源消耗较大

  b、无法回收循环引用的对象,时间开销大(当前的引用计数需要去维护一个数值的变化,时刻监控当前引用数值是否修改,修改需要时间)

  JavaScript 引擎可以通过多种方法来优化垃圾回收的效率,例如采用增量式垃圾回收算法,使用后台线程进行垃圾回收,等等。这些优化方法可以帮助 JavaScript 引擎更快地回收垃圾,提高程序的性能。

总结

  以上就是本文的全部内容,希望给读者带来些许的帮助和进步,方便的话点个关注,小白的成长之路会持续更新一些工作中常见的问题和技术点。

 

posted @ 2021-02-28 11:46  zaisy'Blog  阅读(621)  评论(0编辑  收藏  举报