资讯专栏INFORMATION COLUMN

js闭包的本质

qianfeng / 3210人阅读

摘要:也正因为这个闭包的特性,闭包函数可以让父函数的数据一直驻留在内存中保存,从而这也是后来模块化的基础。只有闭包函数,可以让它的父函数作用域永恒,像全局作用域,一直在内存中存在。的本质就是如此,每个模块文件就是一个大闭包。

为什么会有闭包

js之所以会有闭包,是因为js不同于其他规范的语言,js允许一个函数中再嵌套子函数,正是因为这种允许函数嵌套,导致js出现了所谓闭包。

function a(){
    function b(){
    
    };
    b();
}
a();

在js正常的函数嵌套中,父函数a调用时,嵌套的子函数b的结构,在内存中产生,然后子函数又接着调用了,子函数b就注销了,此时父函数a也就执行到尾,父函数a也会把自己函数体内调用时生成的数据从内存都注销。

function a(){
    function b(){
    
    }
    return b;
}
var f=a();

这个例子中,父函数调用时,函数体内创建了子函数b,但是子函数并没有立即调用,而是返回了函数指针,以备“日后再调用”,因为“准备日后调用”,此时父函数a执行完了,就不敢注销自己的作用域中的数据了,因为一旦注销了,子函数b日后再调用时,沿着函数作用域链往上访问数据,就没有数据可以访问了,这就违背了js函数作用域链的机制。

正因此,子函数要“日后调用”,导致父函数要维持函数作用域链,而不敢注销自己的作用域,那么这个子函数就是“闭包函数”。

闭包函数在形式上有很多种。

在这个例子中,父函数v()体内定义了好几种子函数,这些子函数有的是异步事件的回调函数,会进入浏览器的事件循环池,等主线程工作结束后日后再调用这些回调函数,这些子函数,都导致父函数调用完了,不敢注销自己的作用域,因此这些子函数都是闭包函数。

js并不是为了创造闭包而创造,完全只是因为js允许函数嵌套,js函数嵌套还有个函数作用域链的机制,让父函数不敢注销自己作用域中的数据,才会产生所谓闭包。

也正因为这个闭包的特性,闭包函数可以让父函数的数据一直驻留在内存中保存,从而这也是后来js模块化的基础。

闭包与函数作用域

如果仅仅只是有函数嵌套,而没有函数作用域链,也或许不会有闭包。理解js函数作用域至关重要。

function a(){

}

函数的作用域实际上是个动态概念,上面的代码,只是定义了一个函数,并没有调用函数,函数的作用域是不存在的。只有函数a调用时,才会在内存中动态开辟一个自己的作用域,函数调用完了这个作用域又关闭了,函数运行过程中在内存创建的数据又被清除了。

function a(){
    var n=1;
    function b(){
        n++;
        console.log(n);
    }
    b();
    b();
    b();
}
a();

这个例子中,父函数a调用,首先在内存中动态开辟了作用域,然后在运算过程中,定义了函数b,子函数b()每次调用,都会开辟自己的作用域,在自己的作用域内进行运算,运算过程中访问了还处于打开状态的父函数作用域中的变量n的值,这个子函数三次调用,每次调用时候自己子作用域,访问的都是同一个变量n。但是父函数a总有执行完的时刻,总有要关闭作用域的时候。

var q="";
function a(){
    var n=1;
    q=function b(){
        n++;
        console.log(n);
    }
}
a();
q();
q();
q();

这个例子中,运行父函数,函数开启了作用域,运算过程中生成了函数b,子函数b赋给了全局变量q,导致父函数a运行完了,不敢关闭自己的作用域,,让子函数b成了闭包函数,全局变量q持有了这个闭包函数。

这个得到的结果,和上面例子中常规函数嵌套,得到的效果是一样的。但是区别在于,前一个例子中,父函数a即便执行万年,也有结束要关闭作用域的时候,而这个闭包,就让它的父函数作用域永恒了。

实际上在js的作用域机制中,有一个作用域是永恒的,就是window全局作用域,只要浏览器窗口不关闭,这个windows全局作用域就是永恒的,在全局作用域中定一个函数,无论调用几次,这几次调用都可以共享操作同一个全局变量。除了window作用域可以永恒,其他的函数作用域,总有关闭的时候而无法永恒。只有闭包函数,可以让它的父函数作用域永恒,像windows全局作用域,一直在内存中存在。

当闭包函数调用时,它会动态开辟出自己的作用域,在它之上的是父函数的永恒作用域,在父函数作用域之上的,是window永恒的全局作用域。闭包函数调用完了,它自己的作用域关闭了,从内存中消失了,但是父函数的永恒作用域和window永恒作用域还一直在内存是打开的。闭包函数再次调用时,还能访问这两个作用域,可能还保存了它上次调用时候产生的数据。只有当闭包函数的引用被释放了,它的父作用域才会最终关闭(当然父函数可能创建了多个闭包函数,就需要多个闭包函数全部释放后,父函数作用域才会关闭)。

这个例子是闭包函数的一个典型应用,示例中只有两个函数嵌套,但是加上window全局作用于,一共会有三个嵌套作用域。其中for循环了三次,三次调用了匿名自执行函数,就开了三个函数作用域,开第一个作用域时保存的i的值是0,开第二个作用域保存的是1,第三个保存的是2。三次调用父函数,又创建了三个闭包函数,每个闭包函数沿着它自己的作用域链向上访问,访问的值就都不相同。三个闭包函数调用完了,它们自己的作用域就关闭了,但是各自的父函数作用域还一直在内存中处于打开状态,下次闭包函数再调用时,再接着访问它自己的作用域。就像在window全局作用域定义了一个函数,函数调用几次,全局作用域都在,每次调用都接着访问全局作用域。

闭包与js模块化

日常编码中有很多地方会不经意用到了闭包只是没有察觉,使用闭包的作用就是为了两点:形成命名空间同时保存数据。

在HTML中引入多个js文件,浏览器会从第一个执行到最后一个,这些js文件都共用一个全局作用域,这很多时候就会导致命名冲突。而如果只是为了命名空间,匿名自执行函数也可以实现。

(function(){
    var a=1;
})()
alert(a);//访问不到变量a的值,会报错变量a未定义

这个例子中就借助匿名自执行函数实现了命名空间,隔离了数据,不会产生冲突,但是仅仅只是把数据封起来不提供接口有些时候或许也不行,因此这就需要闭包。

这个例子在前一个例子基础上进行了改造,a.js文件中就使用了闭包,无论这个文件引入到哪里,它的数据都是隔离的,不会会任何地方的代码产生冲突,同时它提供了闭包函数作为API接口,让其他地方以指定的方式访问数据,得到需要的结果,其他地方也不需要关心闭包结构里的数据是什么或者怎么操作的,也不需要担心引入它会与自己的代码冲突。

require.js的本质就是如此,每个模块文件就是一个大闭包。

是否使用闭包要考虑两点:隔离和数据保存。如果需要隔离数据形成命名空间,可以使用匿名自执行函数。如果需要隔离数据,同时还需要在隔离状态保存数据,保存了后面还可以继续使用,那就可以使用闭包。如果都不需要,那就使用普通函数,函数调用完作用域就关闭数据就释放了,没有保存,数据不存在了也不需要隔离了。

一个实际应用

淘宝的购物车中,一个商品点击新增数量或减少数量,它会往服务器发送一个请求保存新数量,但是如果快速连续点击,淘宝的购物车并没有跟随快速点击连续发送ajax,而是在连续点击的结束之后才发送了一个请求,把用户真正想要的数量最后才用一个请求发送了服务器,这样就减少了不必要的请求减少服务器的压力。

如果只是单纯用个click事件处理函数,然后把ajax放到处理函数中,点一次按钮就会发一次请求,连续点就会连续发。而要实现淘宝的这个效果,它要的原理是,定一个延时时间,比方1秒,单击之后过1秒种才发请求,而如果单击了之后还没有到1秒又连续单击了,那么重置这个计时,快速连续单击就一直再重置这个计时始终都没有达到一秒,就不会因为连续点击而发送请求,直到最后连续点击停下来了,过了一秒才发一个请求。

这个应用中就借助了闭包函数,实际click事件真正执行的用于发送请求的也就是里面嵌套的红框的闭包函数,每一次单击都会执行这个红框函数,它除了最终发送ajax,还要做个判断,如果上一次点击的时间,到这一次又点击的时间,这之间的间隔小于了指定的1秒,那么就不会发送ajax,同时重置这个计时。而在最初第一次单击的时候,它还需要上一次的时间,这个时间就只能在初始化时候用一个变量保存一个当前时间,然后第一次单击时候的时间与变量保存的时间进行一个对比。单击第二次时,那么该变量又保存了第一次单击时的时间,然后第二次单击的时间又与第一次单击的时间进行比较。

关键也就在于需要个变量保存上一次的时间。这时间不借助闭包函数也完全可以,就把这个变量放在全局环境下,在全局环境下定义一个全局变量startTime,反正就是保存一下上一次单击的时间。但是问题在于,购物车中有多个商品,并不会有只有一个单击按钮需要用到这个,多个按钮要用,给每个按钮都定义全局变量,startOne,startTwo,startThree...那就很麻烦,并且通过json渲染多个商品时候也不可能手动去定义这么多变量。这就必需借助闭包函数。

json在渲染多个商品时按钮时,这个debounce函数就会被多次调用,每一次调用都return返回了一个闭包函数给每个商品的button按钮的click作为其处理函数,那么每个处理函数都有一个专属的永恒父作用域,并且里面都已经自动定义了各自需要使用的startTime变量用于保存每个按钮自己计算时使用的上一次单击的时间。通过闭包解决这个问题这就非常方便。

额...

上面一个通过for()循环创建多个闭包函数,内存开多个作用域来保存不同的数据,不一定是最好的实现。这个例子中,同样是for循环创建三个了函数,但三个函数都是普通函数。由于函数在js中也是对象,因此给函数本身创建一个静态属性来保存不同的值,那么for循环创建的三个普通函数,每个函数的静态属性都保存了不同的值,而不必借助闭包结构保存不同的值,可以减少内存消耗。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88140.html

相关文章

  • 详解js闭包

    摘要:定义函数的时候,为什么的值重新从开始了因为又一次运行了函数,生成一个新的的活动对象,所以的作用域链引用的是一个新的值。 前言 在js中,闭包是一个很重要又相当不容易完全理解的要点,网上关于讲解闭包的文章非常多,但是并不是非常容易读懂,在这里以《javascript高级程序设计》里面的理论为基础。用拆分的方式,深入讲解一下对于闭包的理解,如果有不对请指正。 写在闭包之前 闭包的内部细节,...

    chaosx110 评论0 收藏0
  • 一次阿里面试后对函数本质理解

    摘要:函数使用函数的使用主要有两种闭包闭包的本质是对共享变量的操作,典型运用是观察者模式备忘录模式普通封装,复用。参考阿里博客你可能不知道的事基础篇总结要写好一个项目需要兼容,性能,安全等。 一次阿里面试后对函数本质的理解 写在前面 环境:阿里的在线编程系统允许面试官在线考察面试者的编程能力. 考点:编程和理论. 编程:分为技术自驱力、异步操作、编程风格(颗粒小)、变量作用域、DOM操作...

    liuyix 评论0 收藏0
  • 一次阿里面试后对函数本质理解

    摘要:函数使用函数的使用主要有两种闭包闭包的本质是对共享变量的操作,典型运用是观察者模式备忘录模式普通封装,复用。参考阿里博客你可能不知道的事基础篇总结要写好一个项目需要兼容,性能,安全等。 一次阿里面试后对函数本质的理解 写在前面 环境:阿里的在线编程系统允许面试官在线考察面试者的编程能力. 考点:编程和理论. 编程:分为技术自驱力、异步操作、编程风格(颗粒小)、变量作用域、DOM操作...

    superw 评论0 收藏0
  • 一次阿里面试后对函数本质理解

    摘要:函数使用函数的使用主要有两种闭包闭包的本质是对共享变量的操作,典型运用是观察者模式备忘录模式普通封装,复用。参考阿里博客你可能不知道的事基础篇总结要写好一个项目需要兼容,性能,安全等。 一次阿里面试后对函数本质的理解 写在前面 环境:阿里的在线编程系统允许面试官在线考察面试者的编程能力. 考点:编程和理论. 编程:分为技术自驱力、异步操作、编程风格(颗粒小)、变量作用域、DOM操作...

    jeyhan 评论0 收藏0
  • 简述作用域还有闭包延伸至模块化

    摘要:首先变量对于一个程序来说是一个很重要的角色那么问题来了这些变量存在哪里程序用到的时候如何找到变量呢所以需要一套规则来存储变量方便之后再找到这套规则就成为作用域是一门编译语言对于来说大部分情况下编译发生在代码执行前的几微妙的时间内对于参与到一 首先,变量对于一个程序来说是一个很重要的角色, 那么问题来了 这些变量存在哪里,程序用到的时候如何找到变量呢? 所以需要一套规则来存储变量方便之后...

    imingyu 评论0 收藏0

发表评论

0条评论

qianfeng

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<