对于闭包的理解,这样理解很加分

317 阅读5分钟

前端面试准备中绕不过的一道经典问题。谈一谈对闭包的理解

面试过程中可以先讲讲闭包概念,闭包的形成,再到它的应用去说明它。如果不需要查看测试过程,可以直接了解概念、形成、特点以及应用

(重点在闭包应用,可以凸显对其概念理解,本章用到了闭包在vue2响应式原理的Object.defineProperty ()中的应用,可以在回答这个问题的时候,同时聊一下vue2原理的一些认识,起到一题答两题的效果)

image.png

闭包的概念 / 形成 / 特点

  1. 概念

通过一些搜索,我们可以了解到一个很专业的介绍:闭包(Closure)是一个函数以及其相关的引用环境的组合。 简单来说,内部函数可以读取到其他函数的变量,并延长变量的生命周期

我自己的理解来说:闭包的本质是一个对象,该对象用于存储内部函数引用外部函数的局部变量

  1. 形成
  • 函数嵌套
  • 内部函数引用外部函数的局部变量
  • 内部函数必须是有效函数
  1. 特点
  • 延长外部函数的局部变量的生命周期
  • 会占用内存
  • 如果使用不当,可能会导致内存泄漏及溢出
  • 使用完闭包,要及时销毁,让内部函数成为垃圾对象
  1. 应用(简单过程,具体看下面闭包应用)
  • Object.defineProperty(数据,新属性,值)接收的值
  • 通过里面的get set函数,对新属性值进行访问修改
  • 需要一个中间变量进行中转
  • 闭包封装

闭包的应用

闭包其实在开发中应用非常的多,比如防抖节流,生命周期函数中如果在自定义函数并且使用当前组件实例中的数据等,而接下来介绍的是 闭包在vue2中响应式原理方面一个体现。

image.png

vue2响应式原理: 静态方法Object.defineProperty ()

vue2 的响应式原理,主要用到了一个构造函数的静态方法 Object.defineProperty ()这个方法可以定义对象的属性,包括属性的值、可写性、可枚举性和可配置性等。在 Vue2 中,它主要被用来进行数据劫持,从而实现数据的响应式。

简单理解Object.defineProperty ()的使用

简单对此方法的使用理解

image.png image.png
理解Object.defineProperty方法中get 和 set函数

除了上面的简单的对Object.defineProperty方法的使用,其中有两个函数get()set(),分别代表要去访问这个值 和 设置这个值,这两个函数是关键之一(这里看到明明是自增但是还是get到了,因为自增也在访问。如果直接赋值就只会触发set

image.png image.png

如果我们把每一个变量都绑上这样一个东西,变量就同时拥有了被访问和被修改的功能。接下来进行赋值操作(对a访问触发了get,进行赋值触发了set,但是如果赋值后再次访问,此时a的值依然为原值)

image.png image.png image.png image.png

对于上面这种情况,我们需要用一个值进行中转,让a的值发生变化,最简单的方法就是在外面设置一个变量进行中转

image.png image.png

简单总结一下以上内容,通过Object.defineProperty的方法实现了对数据的劫持追踪,通过设置一个中间变量,来实现赋值。(此时可以看到这个函数我们可以将其重新的打包封装,但是外部定义的方式不太适合后续定义多了的情况,这时候就用到了本章的主角闭包

通过闭包对上面提到的函数进行封装

image.png

image.png

案例实现:加深对闭包的理解(闭包和没有闭包情况)

我们最常见,比较方便理解闭包的一个案例,一个函数内部包含函数,内部函数调用外部函数的变量,外部函数执行结束,变量本应该被摧毁,但是因为形成了闭包,内部函数依然可以访问这些变量,变量被 ‘封闭’ 在了内部函数中。从而延长了变量的生命周期 对比案例:内部函数不引用外部函数的变量

测试内容
测试环境html(在浏览器开发工具 内存 中通过快照去观察内存情况)
测试目的观察闭包和非闭包情况下,两者内部变量是否被销毁。
测试过程记录点击前内存情况,点击后内存情况

image.png

闭包按钮测试过程
  • 1(快照,无操作) =====> 2(闭包按钮,快照)【分别查看两次快照中内存中是否存在变量】
image.png

操作前后对比图:函数闭包,执行完,变量并没有销毁依然存在

image.png image.png
非闭包测试按钮测试过程
  • 3(快照,无操作) =====> 4(不闭包按钮,快照)【分别查看两次快照中内存中是否存在变量】

操作前后对比图:函数不闭包,执行完,变量销毁,查询不到 image.png

image.png

测试代码

闭包应用

var obj={
}
function defineReactive(target,key,value){
   return Object.defineProperty(target,key,{
        get(){
            return value
        },
        set(newValue){
            value=newValue
        }
    })
}
//此时 外部函数的value就是内部函数引用的变量
// 因为形成了闭包,这个value不会因为外部函数的执行完而销毁
// 实现了数据响应式
defineReactive(obj,'a',1)
// 访问
console.log('修改前',obj.a)
// 修改 
obj.a = 100
console.log('修改后:',obj.a)

闭包测试

   <button id="closureBtn">闭包</button>
    <button id="noClosureBtn">没有闭包</button>

    <script>
        // 有闭包的情况
        function closure1() {
            let superUniqueLongNameThatShouldBeEasilyIdentifiableInHeapSnapshot  = 'closure function 1111111';
            console.log("外部函数执行完毕---1  ");
            return function closureInner1() {
                console.log("外部函数执行完毕---1: " + superUniqueLongNameThatShouldBeEasilyIdentifiableInHeapSnapshot );
            }
        }
        // 这里用到了 比较长的变量名,为了与其他变量名不同,在快照里方便查找
        // 没有闭包的情况
        function closure2() {
            let superUniqueLongNameThatShouldBeEasilyIdentifiableInHeapSnapshot222  = 'not closure function 2222222';
            console.log("外部函数执行完毕---2");
            function closureInner2() {
                console.log('closureInner2---2,此时没有引用外部函数的变量,没有形成闭包');
            }
            closureInner2();
        }

        // 获取按钮并绑定点击事件
        document.getElementById('closureBtn').addEventListener('click', function() {
            const closureFn = closure1();
            closureFn(); // 执行闭包函数
        });

        document.getElementById('noClosureBtn').addEventListener('click', function() {
            closure2();  // 执行没有闭包的函数
        });
    </script>