前端-闭包

闭包(Closure)是JavaScript中一个非常重要且强大的概念,理解闭包对于掌握JavaScript高级特性至关重要。下面我将从多个角度详细解释闭包的概念、原理和应用。

一、什么是闭包?

闭包是指那些能够访问独立 (自由) 变量的函数,或者说函数定义时的词法环境(lexical environment)而非调用时的环境。

访问独立变量:函数能访问不属于自己作用域的变量

定义时而非调用时:这就是 JavaScript 的词法作用域规则。

闭包 = 函数 + 函数定义时所在的词法环境

本质:函数能记住并访问创建时的词法作用域,即使在作用域外执行,依然可以访问外部变量,这就是闭包。

1.1 词法作用域(Lexical Scoping)

JavaScript 的作用域是"静态"的。

函数在定义的那一刻,就确定了它能访问哪些变量,跟在哪里调用无关。

=> 函数的作用域由其在代码中的书写位置决定(定义时)。

javascript 复制代码
function outer() {
  const name = 'Outer';
  // inner 函数就是闭包
  // inner函数形成了对outer函数中name变量的闭包
  function inner() {
    console.log(name); // 访问外部函数的变量
  }
  return inner;
}

const closureFunc = outer();
closureFunc(); // 输出: Outer

在上面的例子中,inner函数在 outer函数内部定义,它就"记住"了 outer的作用域。即便 outer执行完了,inner依然能访问 name。

二、 底层揭秘:V8 引擎眼中的闭包

2.1 关键角色:\[Environment]

在 JS 引擎(如 V8)中,每个函数都有一个内部隐藏属性 \[Environment]。

\[Environment] 是函数在创建时 保存的一个引用,指向其外层的词法环境(Lexical Environment)

当函数执行时,会创建一个 执行上下文(Execution Context),其中包含:

  • 词法环境(Lexical Environment) :存储变量 (let/const/function)
    • 函数定义时(不是调用时),会通过内部属性 \[Environment] 保存当前词法环境的引用 。
  • 变量环境(Variable Environment):存储 var
  • outer 指针:指向上一层词法环境,形成作用域链
text 复制代码
全局环境 ← outer 环境 ← inner 环境
  • outer 执行完毕,执行上下文弹出调用栈
  • 但 inner 的 \[Environment] 仍引用 outer 的词法环境
  • 垃圾回收机制(GC)不会回收该环境
  • 变量会被保留,不会销毁

2.2 闭包的形成过程

我们用一段经典代码来拆解:

js 复制代码
function createCounter() {
  let count = 0; // 被闭包"捕获"
  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();
counter();

Step 1:函数定义阶段

createCounter 定义,内部匿名函数定义时,它的 \[Environment] 指向 createCounter 的词法环境。

Step 2:执行 createCounter()

  • 创建 createCounter 执行上下文
  • 创建词法环境,包含 count = 0
  • 返回内部匿名函数
  • 匿名函数带着 \[Environment] 一起被返回

Step 3:createCounter执行完毕

  • 正常情况下,函数执行完,执行上下文出栈,局部变量销毁
  • 但闭包阻止了销毁
  • 因为匿名函数的 \[Environment] 仍在引用 count
  • GC(V8 的垃圾回收器) 判定变量可达 → 词法环境被保留

Step 4:执行 counter()

  • 匿名函数执行
  • 在自身作用域找不到 count
  • 沿着 \[Environment] 找到保留的词法环境
  • 读取并修改 count

✅ 结论:闭包在 V8 中,本质上是被函数捕获并延长生命周期的词法环境

三、 作用域链 + 闭包

text 复制代码
Global Context (全局执行上下文)
├── LexicalEnvironment (词法环境)
│   ├── createCounter: function
│   └── counter: function (指向 inner (内部)函数)
│
└── createCounter 执行后 (环境未被销毁!)
    ├── LexicalEnvironment (词法环境 依然存活)
    │   ├── count = 1 (被修改后的值)
    │   └── inner: function (内部函数)
    │       └── [[Environment]] ──────────────┐
                                        │
counter() 执行时:                      	▼
inner Execution Context(内部函数执行上下文)
├── LexicalEnvironment (自身词法环境)
└── Outer Reference (作用域链) ──► createCounter 的 LexicalEnvironment

作用域链本质:沿着 \[Environment] 一层层向上查找变量

闭包本质:让外层作用域不会被回收

四、 闭包的经典应用场景

4.1 模块模式(Module Pattern)

这是早期 JS 实现私有变量的唯一手段。

js 复制代码
const myModule = (function() {
  let privateVar = '我是私有的'; // 外部无法直接访问
  function privateMethod() {
    console.log(privateVar);
  }
  return {
    publicMethod: function() {
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // 我是私有的
// myModule.privateVar; // undefined
// myModule.privateMethod(); // 报错

4.2 函数工厂 & 柯里化(Currying)

js 复制代码
// 函数柯里化
// 1. 定义外层函数 multiply(a),
// 接收第一个参数,暂存起来
function multiply(a) {
 // 2. 返回新函数,形成闭包
  // 内部函数会记住外层作用域的 a
  return function(b) {
    return a * b;
  };
}
// 3. 执行 multiply(2)
// a = 2 被闭包保留,不会销毁
// 返回的函数赋值给 double
const double = multiply(2);
// 4. 执行 double(5)
// 使用闭包中保存的 a=2
double(5); // 10

柯里化本质:

把多参函数拆成一系列单参函数,利用闭包记住前面的参数。

好处:参数复用、逻辑复用、批量生成工具函数。

4.3 防抖与节流(Debounce & Throttle)

前端性能优化高频方案,闭包用于保存定时器 / 时间戳。

js 复制代码
// 防抖函数示例
// 防抖:延迟 delay 后执行,若期间再次触发则重新计时
function debounce(fn, delay) {
  // 闭包保存定时器 ID,避免多次触发重复执行
  let timer = null;

  return function (...args) {
    // 清除之前的定时器
    clearTimeout(timer);
    // 重新设置新定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用示例
const handleInput = debounce(function () {
  console.log('发送搜索请求');
}, 500);

// 输入框频繁输入时,只会在停止输入 500ms 后执行一次
input.addEventListener('input', handleInput);


// 节流(Throttle)
// 核心思想:在 指定时间内,函数只执行一次,不管触发多频繁,都按固定频率执行。
// 常用于:滚动监听、窗口 resize、高频点击、鼠标移动等。
function throttle(fn, interval) {
  // 用闭包保存上一次执行的时间戳
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();
    // 判断当前时间 - 上次执行时间 >= 间隔时间
    if (now - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = now; // 更新最后执行时间
    }
  };
}

// 使用示例:滚动节流
const handleScroll = throttle(function () {
  console.log('滚动触发了', scrollY);
}, 300);

window.addEventListener('scroll', handleScroll);

防抖:等待一段时间,只执行最后一次

节流:固定时间内,只执行一次

两者都必须依靠闭包保存状态(timer /lastTime),否则无法实现。

五、 踩坑指南:闭包的内存陷阱

闭包虽好,但不能滥用。

5.1 循环中的经典坑

js 复制代码
// 错误示例
for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i); // 全部输出 6
  }, i * 1000);
}
// 原因:var没有块级作用域,i是全局变量,所有回调函数共享同一个 i。
✅ 解决方案 1:IIFE(立即调用函数表达式,用于创建独立闭包作用域。)
for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j); // 1, 2, 3, 4, 5
    }, j * 1000);
  })(i);
}
✅ 解决方案 2(推荐):let块级作用域
for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i); // 1, 2, 3, 4, 5
  }, i * 1000);
}
(注:let 在循环中,每一轮都会创建一个新的、独立的词法环境,相当于自动闭包。)

5.2 内存泄漏风险

如果一个闭包持有巨大的数据结构,且长期不释放,会导致内存占用过高。

js 复制代码
function heavyTask() {
  const bigData = new Array(10000000); // 大对象
  return function() { console.log('done'); };
}

const task = heavyTask(); 
// 即使 heavyTask 执行完了,bigData 依然存在
// 内存不释放 → 页面卡顿
task = null; // 手动切断引用,让 GC 回收

JavaScript 闭包保留的是「整个词法环境」,不是「用到的变量」。

只要返回内部函数 → 形成闭包 → 外层所有变量都会被保留。

无论内部函数有没有使用,都不会被回收。

5.3过多闭包可能导致性能问题

闭包变量需要沿作用域链查找,比局部变量稍慢。

大量长期驻留的闭包会增加内存压力,注意及时释放。

六、 总结

闭包的本质:函数 + 定义时的词法环境 的结合体。

形成原理:基于词法作用域,函数通过 \[Environment] 引用外部环境。

V8 视角:闭包是未被垃圾回收的词法环境。

核心价值:数据封装、私有变量、状态持久化。

注意事项:警惕循环陷阱、内存占用,不用时及时置 null 释放。

相关推荐
ct9781 分钟前
ES6 新特性
前端·vue.js·性能优化
KaMeidebaby8 分钟前
卡梅德生物技术快报|抗原如何自己检测?FAdV-4 重组抗原制备与 ELISA 体系技术调试指南
前端·人工智能·物联网·算法·百度
一拳不是超人11 分钟前
AI 辅助研发时代,如何用“规范 Skill”缩短测试周期
前端·人工智能·代码规范
夜郎king2 小时前
湖南高考天气查询:基于 HTML5 与百度天气 API 实现页面展示
前端·html5·百度天气实践·天气信息可视化
云水一下9 小时前
TypeScript 从零基础到精通(五):高级类型与泛型
前端·javascript·typescript
counterxing9 小时前
vibe coding 之后,我更不想打字了
前端·agent·ai编程
copyer_xyf10 小时前
Python 模块与包的导入导出
前端·后端·python
研☆香10 小时前
es6新特性功能介绍(四)
前端·ecmascript·es6
微扬嘴角10 小时前
React篇1--JSX语法规则、组件、组件实例的3大特性
前端·react.js·前端框架
copyer_xyf10 小时前
Python venv 虚拟环境
前端·后端·python