JS闭包详解

一、什么是闭包?

用一句话概括:闭包就是"函数" + "它能访问的外部变量环境"。

在 JavaScript 中,函数内部可以访问外部的变量,但外部无法访问函数内部的变量。当一个内部函数被返回到外部,或者被传递到其他地方时,它不仅带走了自己的代码,还"打包"带走了它出生时所在的外部变量。这个"打包带走"的动作和结果,就是闭包

二、为什么需要闭包?(实际开发场景)

在实际开发中,我们通常用闭包来实现两个目的:

  • 保护变量不被污染(实现私有变量);
  • 让变量的值一直保存在内存中(状态保持)。

三、贴近实际开发的例子

场景一:实现一个"计数器"(状态保持)

假设你在做一个购物车,需要记录用户加了多少次商品。如果用全局变量,很容易被别人不小心改掉。用闭包就可以完美解决:

javascript 复制代码
function createCounter() {
  let count = 0; // 这个变量被"保护"起来了,外面直接访问不到
  
  // 返回一个对象,里面包含操作 count 的方法
  return {
    increment: function() {
      count++;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const myCart = createCounter();
console.log(myCart.getCount());      // 0
console.log(myCart.increment());     // 1
console.log(myCart.increment());     // 2
// console.log(count);               // 报错!外部根本找不到 count 变量

原理解析: createCounter 执行完后,按理说里面的 count 变量应该被销毁。但因为返回的 incrementgetCount 函数还在使用 count,JavaScript 引擎就会把 count 连同这两个函数一起"闭包"起来,保存在内存中。

场景二:React/Vue 中常见的"防抖函数"(封装工具)

javascript 复制代码
// 封装一个防抖工具函数
function debounce(fn, delay) {
  let timer = null; // 闭包变量,用来存定时器
  
  return function(...args) {
    // 每次触发都清除上一次的定时器
    if (timer) clearTimeout(timer);
    
    // 重新设置定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 实际使用
const handleSearch = debounce((keyword) => {
  console.log('发送请求:', keyword);
}, 500);

// 用户快速输入
handleSearch('a');
handleSearch('ab');
handleSearch('abc'); // 只有这个会在 500ms 后真正执行

**原理解析:**这里的 timer 变量就是闭包的核心。每次调用 handleSearch,它都能"记住"上一次的 timer 是什么,从而做到清除和重置。如果没有闭包,timer 每次都会变成一个新的局部变量,防抖就失效了。

四、避坑指南

闭包虽然好用,但有一个致命缺点:内存泄漏

因为闭包会把变量一直留在内存里,如果滥用闭包(比如在循环里疯狂创建闭包又不释放),会导致网页越来越卡,甚至崩溃。

  • 用完闭包后,如果不再需要,可以手动把引用它的变量赋值为 null(比如 myCart = null),告诉垃圾回收器可以清理了。
  • 不要为了用闭包而用闭包,现代 JavaScript 有了 class模块系统import/export),很多以前需要闭包实现的私有变量,现在有更优雅的写法。

方案一:ES6 Class + # 私有字段(最推荐 ⭐)

在 ES2022 中,JavaScript 引入了真正的语法级私有字段,只需要在变量名前面加一个 # 号。

javascript 复制代码
class ShoppingCart {
  // 使用 # 声明私有变量,外部绝对无法访问
  #count = 0; 

  increment() {
    this.#count++;
    return this.#count;
  }

  getCount() {
    return this.#count;
  }
}

const myCart = new ShoppingCart();
console.log(myCart.increment()); // 1
// console.log(myCart.#count);   // ❌ 报错!语法级别禁止外部访问

为什么更优雅?

以前用闭包,你需要写一个外层函数包裹,再返回对象,代码嵌套深。现在用 Class,结构一目了然,而且 #count 是"硬私有",外部连看都看不到,比闭包更安全。

方案二:ES Module 模块系统(适合工具类 🛠️)

如果你的计数器是一个独立的工具文件(比如 cart.js),你可以利用 ES 模块天然的作用域隔离。

javascript 复制代码
// cart.js
// 没有 export 的变量,天然就是私有的!
let count = 0; 

export function increment() {
  count++;
  return count;
}

export function getCount() {
  return count;
}
javascript 复制代码
// main.js
import { increment, getCount } from './cart.js';

console.log(increment()); // 1
// console.log(count);    // ❌ 报错!外部根本拿不到 count

为什么更优雅?

你不需要写任何包裹函数,只要不写 export,这个变量就对外界隐身了。这是目前前端工程化(如 Vue、React 项目)中最常用的私有化方式。

方案三:WeakMap 模拟私有(适合老项目兼容 🕰️)

如果你的项目需要兼容非常老的浏览器,不能用 #,可以用 WeakMap 结合原型来实现。

javascript 复制代码
const privateData = new WeakMap();

class ShoppingCart {
  constructor() {
    // 把私有数据存在 WeakMap 里,以当前实例为 key
    privateData.set(this, { count: 0 }); 
  }

  increment() {
    const data = privateData.get(this);
    data.count++;
    return data.count;
  }
}

为什么提这个?

纯闭包每次创建实例都会生成新的函数,内存开销大。而 WeakMap 方案把方法放在原型(prototype)上复用,只把数据隔离,在老项目中能兼顾复用与私有。

方案该怎么选?

  • 写组件、写业务对象 :无脑选 Class + # 私有字段。语义最明确,IDE 提示最友好,别人一看就知道这是私有变量。
  • 写工具函数、独立模块 :用 ES Module (不 export)。代码最干净,符合现代前端工程规范。
  • 什么时候还用闭包? :当你需要动态生成一些带有独立状态的函数时(比如我们前面讲的防抖函数 debounce,每次调用都需要一个全新的 timer),闭包依然是无可替代的神器!
相关推荐
何以解忧,唯有..1 小时前
Python协程详解:从生成器到async/await的完整指南
开发语言·python
奶茶树1 小时前
【C++】13. C++11新特性【上】
c语言·开发语言·c++·git·github
SomeB1oody1 小时前
【RustyML入门】7.4. 按需裁剪与模块化集成
开发语言·后端·机器学习·rust·教程
'2061 小时前
**写了个自动整理下载视频的脚本:按片名归档、自动重命名、生成目录索引
开发语言·python·ffmpeg·音视频·cctv
xys_6782 小时前
深挖C语言:动态内存管理
c语言·开发语言
粥里有勺糖2 小时前
视野修炼-技术周刊第130期 | 光影效果
前端·javascript·github
OPEN-F2 小时前
Python进阶教程:项目工程化与虚拟环境
开发语言·python
吴声子夜歌2 小时前
Java面试——基础
java·开发语言·面试
全栈项目管理程序猿2 小时前
ArcGIS JS 基础教程(13):屏幕坐标与地理坐标互转
javascript