前端-闭包

闭包(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 释放。

相关推荐
军军君011 小时前
数字孪生监控大屏实战模板:交通云实时数据监控平台
前端·javascript·css·vue.js·typescript·前端框架·echarts
DanCheOo1 小时前
从脚本到 CLI 工具:用 Node.js 打造你的第一个 AI 命令行工具
前端·aigc
木斯佳1 小时前
前端八股文面经大全:腾讯PCG前端暑期二战一面·深度解析(2026-04-22)·面经深度解析
前端·面经·实习
十一.3662 小时前
012-014 对state的理解,初始化state,react中的事件绑定
前端·react.js·前端框架
你脸上有BUG2 小时前
SSE库选型+fetch-event-source示例
前端·sse·通知订阅
Never_every992 小时前
8 个高清 4K 视频素材网址!无水印可商用
大数据·前端·音视频·视频
NotFound4862 小时前
分享实战中Python Web 框架对比:Django vs Flask vs FastAPI
前端·python·django
冰暮流星2 小时前
javascript之表单事件1
开发语言·前端·javascript
Dalydai2 小时前
AI 辅助前端开发:两个月踩坑实录
前端·ai编程