最近很长一段时间因为各种琐事,已经很久没有更新博客了,其实一直在工作中,但是一方面是没时间更博客,另一方面也是因为没有什么特别想写的点,最近看了下这篇文章在 JS 文件和 Vue 组件中使用防抖节流函数_流光D的博客-CSDN博客还是有不少流量的,评论有人提问
我想知道这个形成的闭包怎么清除掉,直接给这个闭包赋值为null吗
刚好提示了我可以写一篇关于理解 JS 闭包的文章,笔者将会会在近期更新出来。
回到上述问题,我们说 JS 中的防抖节流工具函数的实现原理是一般是利用 JS 闭包特性来实现的,当然也可以不用闭包,如下例子就没有使用闭包实现了简单的防抖:
- // vue组件中
- data () {
- return {
- timer: null
- }
- },
- methods: {
- getXX () {
- if (this.timer) {
- clearTimeout(this.timer)
- }
- this.timer = setTimeout(() => {
- // 具体的业务代码
- }, 500) // 假设防抖间隔是500ms
- }
- }
当然,这是一个 vue 文件。JS 闭包在代码中起到的作用,简单的说主要有两点,一是保护;二是保存。详见笔者之后更新的文章。而在本文中笔者介绍到的防抖工具函数的实现:
- function debounce(func, wait, immediate) {
- var timeout
-
- return function () {
- var context = this;
- var args = arguments;
-
- if (timeout) {
- clearTimeout(timeout);
- }
- if (immediate) {
- // 如果已经执行过,不再执行
- var callNow = !timeout;
- timeout = setTimeout(function(){
- timeout = null;
- func.apply(context, args) // 这行代码可不加,详细说明见下文
- }, wait)
- if (callNow) func.apply(context, args)
- }
- else {
- timeout = setTimeout(function(){
- func.apply(context, args)
- }, wait);
- }
- }
- }
debounce 函数中定义了一个名为 timeout 的局部变量,该函数接受三个参数,分布是 func (作为需要被防抖处理的函数),wait(防抖时间),immediate(执行防抖处理时,是否先立即触发一次被防抖处理的函数)。debounce 函数返回了一个匿名函数(以下简称匿名函数 A):

在返回的匿名函数 A 中,使用了其父作用域的变量 timeout, 由此,本该在 debounce 函数执行完后被 JS 垃圾自动回收系统回收的 timeout 没有被回收,而是被保存在了内存中。于是形成了我们常说的闭包:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
在匿名函数 A 中,timeout 被用来作为 setTimeout 函数的返回值,因为防抖函数要实现的效果是在高频次尝试触发某个行为时,阻止该行为发生,而在一段时间(wait)后都没有尝试触发该行为时,再执行该行为(生活中这样的例子很常见,比如公交车司机停车后等待乘客全部上车后关门),因而我们需要 timeout 变量保存之前的 id 值(有 id 值存在则说明在此之前有尝试触发过该行为)。所以我们在实现上既可以通过在全局作用域声明 timeout, 也可以通过形成闭包来保存 timeout 的值。从函数封装的角度来说,通过闭包来实现防抖更为优雅。
回到最开始的问题,debounce 函数形成的闭包如何清除?
答:并不需要清除 debounce 函数所形成的闭包,因为 debounce 函数被使用的本意便是利用其形成的闭包保存 timeout 这个变量,通过 timeout 变量的值来进行逻辑判断。这里还有一个问题:如果非要回收 debounce 函数中的 timeout 变量呢?比如说当我们明确在 debounce 函数已经被调用了之后,我们心里不舒服,觉得 timeout 没有被系统回收很不爽,那我们可以回收 timeout 变量吗?当然可以,为了便于理解我们写一个简单的闭包例子
- function a(){
- var b = 0;
- setTimeout(() => {
- // alert(b)
- debugger
- }, 3000)
- return function(){
- b++;
- console.log(b);
- }
- }
- var d = a();
- d();//1
- d();//2
- d();// 3
- d = null // ? 可以回收 变量b 吗
-
如上所述,如果把 d 赋值为 null, 变量 b 会被回收吗,我们用 setTimeout 加 debugger 的方法测试:

发现 变量b 并没有被回收,仍然是 3.
因而通过将 d 赋值为 null 的方式不能回收变量b, 于是笔者想到另一种方法,如下:
- function a(){
- var b = 0;
- setTimeout(() => {
- // alert(b)
- debugger
- }, 3000)
- return function(flag){
- if (flag) {
- b = null
- return
- }
- b++;
- console.log(b);
- }
- }
- var d = a();
- d();//1
- d();//2
- d();// 3
- d(true)
-
在返回的闭包中设置一个 flag 变量值,手动清除变量b 的值。

通过 debugger 我们发现变量b 变成了 null, 必然会被系统回收。当然这样的用法很少见,因为一般来说,只要函数d 不是全局函数,在其对应的父作用域被销毁后函数d 的子作用域也必然销毁,变量b 即使不再使用,也无需刻意手动赋值触发自动 GC。不过,如果函数d 是一个全局变量,那么有必要注意形成闭包后是否会导致其一直常驻内存,引起不必要的内存资源浪费。
本文对于 JS 中闭包的出现原因及作用描述还比较浅显,关于闭包形成后保存的变量是否应该回收,如何回收这一块的理解也未十分全面,之后还会更新一篇关于 JS 闭包的文章,对 JS 闭包作更全面深入的讨论 。
参考:
https://www.jianshu.com/p/00c747510df5