闭包深度学习

变量的作用域

要理解闭包,必须要理解js的特殊的变量作用域。变量的作用域分为两种:全局变量和局部变量。
js语言的特殊之处在于,函数内部可以直接读取全局变量

  var n=999;

  function f1(){
    alert(n);
  }

  f1(); // 999

另一方面,在函数外部无法获取函数内部的局部变量

  function f1(){
    var n=999;
  }

  alert(n); // error

PS: 有个地方需要特别注意的,在函数内部声明变量的时候,一定要使用 var 变量,如果不用,则实际上是声明了一个全局变量

  function f1(){
    n=999;
  }
  f1();
  alert(n); // 999

如何从外部获取局部变量

由于种种原因 有时候需要得到函数内部的局部变量,但是前面提到过,正常情况下是办不到的,只有通过变通的方法才能实现。那就是在函数的内部,再定义一个函数

function f1(){
   var n = 999;
   function f2(){
     alert(n) // 999
  }
}

在上面的代码中,函数f2就被定义在函数f1的内部,这是f1内部的所有的局部变量,对f2就是可见的,反之则不可以。这就是js特有的链式作用域结构,子对象会一级一级的向上寻找所有父对象的变量。

function f1(){
  var n = 999;
  function f2(){
    console.log(n)
  }
   return f2
}
var result = f1()
result() // 999

所以因此可见 f2 函数就是闭包

闭包的概念

简单说,闭包就是能够读取其他函数内部变量的函数。在js中,只有函数内部的子函数才能读取读取局部变量,因此可以把闭包理解成定义在一个函数内部的函数。所以闭包就是函数内部与函数外部沟通起来的桥梁。

闭包的用途

闭包可以用在很多地方,最大的用途有两个:1、可以读取函数内部的变量 2、让变量始终保存在内存中 该如何理解呢 下面举例说明

  function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999

  nAdd();

  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

function outer() {
  let count = 0;
  return function inner() {
    count++;
    console.log(count);
  };
}

const fn = outer();
fn(); // 1
fn(); // 2

执行过程分析

步骤 描述
执行 outer(),创建局部变量 count = 0,返回 inner 函数
由于 inner 引用了 count,所以 count 不会被 GC 回收
执行 fn()(即 inner)时访问闭包中的 count,执行 count++
每次调用 fn(),闭包都会在原有的 count 状态基础上累加
若想打印旧值,则用 console.log(count++)

PS:补充说明一点,闭包能够保持变量的状态的本质是外部函数执行完毕后,其局部变量应该被销毁,但只要还有内部函数(闭包)还有引用,js引擎就不会释放这块内存。

闭包使用的注意点

1、由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
举个例子说明如何退出函数前将不用的局部变量删除

function createUser() {
  let name = "张三";
  let age = 25;
  let address = "北京";

  return {
    getName() {
      return name;
    },
    destroy() {
      // 不再需要时,手动清理引用
      name = null;
      age = null;
      address = null;
    }
  };
}

const user = createUser();
console.log(user.getName()); // 张三

// 用完后
user.destroy(); // 手动释放闭包中不再使用的变量

2、闭包就像给函数加了“记忆力”,能在外部访问并修改它内部的变量,但这也可能破坏原本封装好的数据,所以要像保护对象的私有属性一样谨慎使用。
举例:

function createUser() {
  let name = '张三';
  return {
    setName(newName) {
      name = newName;
    },
    getName() {
      return name;
    },
    // ❌ 一个危险的方法
    resetAll() {
      name = null; // 直接清空
    }
  };
}

const user = createUser();
user.setName('李四');
console.log(user.getName()); // 李四

user.resetAll(); // ⚠️ 破坏性操作
console.log(user.getName()); // null ❌

这样 resetAll() 就破坏了原来“name 不该随意清空”的规则。

闭包的实际应用场景

1、防抖和节流
应用特性:闭包能保存状态(保存上一次定时器或时间戳)
防抖:在事件被触发 n 秒后才执行回调,如果在这 n 秒内事件又被触发,则重新计时。
典型场景:输入框联想搜索、窗口 resize、按钮防重复点击。
核心目的:减少频繁调用函数的次数。
节流:规定一个固定时间间隔,在这段时间内无论事件触发多少次,回调函数只执行一次。
典型场景:滚动监听、窗口 resize、鼠标移动事件。
核心目的:控制函数执行频率

2、私有变量/数据封装
应用特性:闭包能访问外部函数的变量,但外部无法直接访问它,实现“私有变量 + 公共方法”。

function createCounter() {
  let count = 0; // 私有变量
  return {
    increment() { count++; },
    getCount() { return count; }
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1

count 只对闭包内部方法可见,外部无法直接访问。
3、模块化开发
应用特性:闭包延长变量生命周期,隐藏内部实现,只暴露公共方法。

const CounterModule = (function() {
  let count = 0; // 私有变量
  function add() { count++; }
  function get() { return count; }
  return { add, get }; // 暴露公共方法
})();

CounterModule.add();
console.log(CounterModule.get()); // 1

count 对外不可访问,只能通过 add() 和 get() 操作。

补充防抖和节流的实现

防抖的实现代码

function debounce(fn, delay) {
  let timer = null;             // A:记住上一次的定时器 id(闭包状态)
  return function (...args) {   // B:返回的函数会在外部被频繁调用
    clearTimeout(timer);        // C:每次调用先把上一次的定时器取消掉
    timer = setTimeout(() => {  // D:建立一个新的定时器:delay 后执行真正的 fn
      fn.apply(this, args);
    }, delay);
  };
}

每次触发都重置一个延时任务,只有最后一次触发后静止达到 delay,真正的 fn 才会被执行。
举例说明:
假设 delay = 5s:

t=0s:第一次触发 → 设闹钟(将在 t=5s 时执行 fn)

t=4s:第二次触发(在第一次的 5s 未到之前) → 先把闹钟关掉(取消第一次),再重新设新的闹钟(将在 t=9s 执行)

t=8s:又触发 → 关掉 t=9s 的闹钟,再设新的(将在 t=13s 执行)

如果从 t=8s 后 没人再触发,那么在 t=13s fn 才会真正执行。

一句话:只有最后一次触发后的完整延时才会触发 fn。
使用示例:
function handleClick() {
console.log("按钮被点击了");
}
const debouncedClick = debounce(handleClick, 2000);

节流的代码实现

function throttle(fn,delay){
     let lastTime = 0;
     return function(..args){
      const now = Date.now();
       if(now - lastTime >= delay){
           fn.apply(this,args);
           lastTime = now
        }
     }
}

循环陷阱

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
// 输出:3 3 3

原因是因为var是函数作用域不是块级作用域,所以循环中的 i 全局只有一个变量,每次循环都会修改这个同一个变量。循环结束时,i = 3(因为循环条件 i < 3 不成立)
时间 i 值 发生的事情
t0 0 第一次循环:执行 setTimeout,回调捕获 i 的引用
t0 1 第二次循环:执行 setTimeout,回调捕获 i 的引用
t0 2 第三次循环:执行 setTimeout,回调捕获 i 的引用
t0 3 循环结束,i = 3
t0+1000ms 3 第一次 setTimeout 回调执行,打印 i → 3
t0+1000ms 3 第二次 setTimeout 回调执行,打印 i → 3
t0+1000ms 3 第三次 setTimeout 回调执行,打印 i → 3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容