一个常考知识点---闭包(回来填坑了)

633 阅读8分钟

对。时隔10个月,回来填坑了......我就是鸽王

闭包这个话题,可能这样聊的话逻辑感好点(面试)。你们可以参考着来,不过这只是我的想法,你们也可以按自己的想法来。 注意区分哪些是我想拓展的内容,哪些是我主要的讲解内容。(分割线隔开了)

1. 给出定义,什么是闭包

内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。

(注:这是我们比较容易接受的定义,可以直接在讲闭包问题的时候,开局就甩出来。)


(这里拓展一下,MDN上对闭包的定义,如果觉得这种好理解,也可以去查查相关资料)

闭包是指那些能够访问自由变量的函数。

举个例子:

var a = 1;

function foo() {

  console.log(a);

}

foo()

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。 那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……

我第一次看到这个观点,是在冴羽大佬的文章里,也可以看看。juejin.cn/post/684490…

2. 怎样产生闭包?

就如定义所言,当函数体内的函数被拿到了函数体外面使用,就形成了闭包。

关于闭包的产生,这个话题我更愿意用一个示例向您说明。

   先定义一个函数aa里面有name、age等属性并赋值,然后里面再定义一个方法sayName,用来打印name,再将这个方法return出来。然后我们再在外部进行调用这个暴露出来的方法,例如foo=a()并执行foo()。
   这样就相当于形成了一个闭包。  
function a(){
  this.name = 'a'
  var sayName = function (){
    consule.log(this.name)
  }
  return sayName
}
var b = a()
b()

3. 闭包是怎样产生的?

依然就刚刚的例子,进行探讨。我们会想,闭包到底是怎么形成的?

(这里分两个版本,一个简单版本,一个进阶版本)


(简单版)

(简单版本的讲解我之前写过文章,也有讲过 ,之前写这个的时候语言还略显青涩,写的比较白话。) juejin.cn/post/696175…

(具体简单版可以看看文章,这里进行简述。)

在a函数执行完之后,垃圾回收机制会询问是否可以完全销毁a的内存,但是a不知道自己什么时候还会被调用,所以会形成一个closure对象,将可能被调用的变量保存起来放到堆内,这样就可以不用继续占用栈内空间。

当真正调用foo方法的时候,就直接到堆内查找相应的变量的储存地址。

这样就实现了闭包的功能。


(进阶版)

其实进阶版更像是具体讲解简单版的实现操作,就是每一个简单步骤,都是依据什么来实现的。

前置知识(放了一些大佬的链接,大家也可以看看,我也是从这些地方学的):

JavaScript的执行上下文栈

JavaScript的作用域链及其实现

V8引擎的垃圾回收机制

(先看看上面的文章,冴羽大佬讲的很好的。)

(这是从执行上下文的角度看闭包的过程。)

  1. 生成一个全局上下文,将全局上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 然后进入全局,进行具体代码执行。执行到a函数,创建a的上下文环境并压入执行上下文栈中。
  4. a上下文初始化,创建变量对象、作用域链(a函数的作用域链属性维护操作,也就是对a.[[scope]]的操作)、this等。
  5. a函数执行完毕,将a函数的执行上下文环境弹出执行上下文栈。
  6. 执行b函数,创建b函数的执行上下文,b函数上下文被压入执行上下文栈
  7. b执行上下文初始化,创建变量对象、作用域链(对b.[[scope]]的操作,先复制外层作用域,再压入自身作用域,从而形成作用域链。根据这个作用域链,我们能够查询到a.AO的值,然后在AO中保存了对相关变量的引用)、this等。
  8. b函数执行完成,b函数上下文从执行上下文栈中弹出。
  9. 全局执行上下文从执行上下文栈弹出。

(然后是一些细节方面的补充,主要是变量如何在堆中储存)

我们会疑惑,本示例中name变量到底储存在哪?为什么会这样储存?

从垃圾回收机制的角度来看,在第五步的时候a上下文环境出栈,通过对其变量的引用进行查询,发现可能会被调用(闭包),因此可能存在引用关系。所以在垃圾回收时将name变量存入新生代储存,经历新生代中Scavenge算法识别后,很快进入到老生代中储存,而老生代是在堆内储存的。

那么接下来,只要这个变量存在引用关系,就能在老生代中被保存下来,也就一直存在于堆内。

而另一边,我们模拟了作用域链的实现,就可以发现,在调用b函数的时候,我们能找到a.AO的值,里面存了对变量name的引用,因此能够实现对变量的调用。

4. 闭包的应用

了解了闭包是什么,那么我们就自然而然会想到闭包的应用。

闭包能够实现一种保护机制,能够使得变量不被销毁。并且闭包将这些变量变成了私有变量(只能由抛出的内部方法调用)而不是一个全局变量。

注:闭包具有“保护”作用:保护私有变量不受外界的干扰(在真实项目,尤其是团队协作开发的时候,应该尽可能地减少全局变量的使用,防止相互之间的冲突(“全局变量污染”)),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量。


单例应用

//js的单例模式 实现登入
    var createLogin  = function(a,b,c){
      console.log(a,b,c); // 只打印一次1,2,3
      var div = document.createElement('div')
      div.innerHTML = '我是登入的弹窗'
      div.style.display = 'none'
      document.body.appendChild(div)
      return div
    }
    var getSingle = function (fn) {
      var result;
      return function(){ // return一个函数里面形成闭包
        // 如果result用的话只执行一次,否则会执行
        return result || (result = fn.apply(this,arguments))
      }
    }
    var create = getSingle(createLogin)
    document.getElementById('loginBtn').onclick = function (){
      var loginLay = create(1,2,3)
      loginLay.style.display = 'block'
    }

防抖函数

**效果:**当持续触发事件 一定时间内没有在触发事件,事件处理函数会执行一次,如果设定时间到来之前 又一次触发事件,就重新开始延时。

**思路:**触发事件 一段时间内,又一次触发了事件,重新开始延时,代表的就是重新开始定时器。在设定时间内,又一次触发了事件 重新开始延时,代表的就是重新开始定时器,那么意味着上一次还没有结束定时器需要清除掉,重新开始。

    //防抖
    // 在一定时间内重复操作的话,只会刷新操作读条,等到最后一次操作后计时执行。
    function mydebounce(fun, delay) {
      let timer;
      return function (args) {
        let that = this;
        clearTimeout(timer); //重复操作后刷新读条
        // 我们应该存储timer变量
        // timer变量需要一直保存到内存中
        // 既然你不想打印之前已经输入的结果,清除以前的触发的定时器
        // 一直在内存当中 内存的泄漏 闭包
        timer = setTimeout(function () {
          fun.call(that, args);
        }, delay);
      };
    }

节流函数

当持续触发的时候 保证一段时间内 只调用一次事件处理函数.

    //节流
    //计算最近一次操作的时间与现在时间的间隔,再与标准比较
    function mythrottle(fun, delay) {
      let pre = 0;
      return function (...args) {
        //同样返回一个函数,并且使用了外部变量
        //形成闭包
        //一直存在对其的引用关系
        let now = Date.now();
        if (now - pre > delay) {
          pre = now;
          fn.apply(this, args);
        }
      };
    }

5. 闭包后引导话题

从整个过程也能看出,简单讲完闭包后,能将话题引导到作用域、作用域链、执行上下文、预编译、V8引擎的回收机制、JS代码的编译执行、this指向等问题,后续就看自己的发挥了。


交个朋友(2022.04.01记)

目前正在疯狂学习前端知识,想要成为更优秀的前端工程师,因此喜欢记录并分享自己的学习笔记。奈何本人知识储备有限,只能做到输出一些些自己的观念。

不过!我真的有做很多很多笔记(确实很多笔记是摘抄而来,因此作为自己的帖子发出不厚道....),我也真的很希望能和志同道合的小伙伴们分享交流更多知识点!

因此我简单搭建了一个自己的博客,希望能结识到更多小伙伴,如果有兴趣,来我博客逛逛吧~ 阿敏的成长日记