js 中的闭包

一、什么是闭包?

  “官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式
 (通常是一个函数),因而这些变量也是该表达式的一部分。

  相信很少有人能直接看懂这句话,因为他描述的太学术。我想用如何在Javascript中创建一个闭
包来告诉你什么是闭包,因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。看下面这段代码:

    function a(){
      var i=0;
      function b(){
        alert(++i);
      }
      return b;
    }
    var c = a();
    c();

  这段代码有两个特点:

  1、函数b嵌套在函数a内部;

  2、函数a返回函数b。

引用关系如下:

  这样在执行完var c=a()后,变量c实际上是指向了函数b,再执行c()后就会弹出一个窗口显示i的值
(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,
就是说:

  当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。

  我猜想你一定还是不理解闭包,因为你不知道闭包有什么作用,下面让我们继续探索。

  二、闭包有什么作用?

   简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回
a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。这是对闭包作用的非常直白的描述,
不专业也不严谨,但大概意思就是这样,理解闭包需要循序渐进的过程。

    在上面的例子中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是
自加1后alert出i的值。

    那么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,
b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引 用,因此函数a和b互相引用但又不
被外界打扰(被外界引用),函数a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后面详细介绍)

  三、闭包内的微观世界

    如果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的
执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。
以函数a从定义到执行的过程为例阐述这几个概念。

   1、当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境
”,如果a是一个全局函数,则scope chain中只有window对象。
  2、当函数a执行的时候,a会进入相应的执行环境(excution context)。
  3、在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的
scope chain。即a.scope=a的作用域链。
  4、然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具
有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最
顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
  5、下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
  6、最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数
b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
  
  到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含
了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b
又依赖函数a,因此函数a在返回后不会被GC回收。

  当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、
a的活动对象和window对象,如下图所示:

   如图所示,当在函数b中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,如果存在则返回,
如果不存在将继续搜索函数a的活动对象,依 次查找,直到找到为止。如果整个作用域链上都无法找到,
则返回undefined。如果函数b存在prototype原型对象,则在查找完自身的活动对象 后先查找自身的原
型对象,再继续查找。这就是Javascript中的变量查找机制。

  四、闭包的应用场景

  1、保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途
径访问到,因此保护了i的安全性。

  2、在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c()
,都会给i自加1。

  以上两点是闭包最基本的应用场景,很多经典案例都源于此。

  五、Javascript的垃圾回收机制

  在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,
而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,
这就是为什么函数a执行后不会被回收的原因。
<think>首先,用户的问题是关于JavaScript闭包的常见示例和用法。我需要根据系统指令来构建回答。 系统指令关键点: - 所有行内数学表达式用 `$...$` 格式,但这里没有数学内容,所以可能不需要。 - 独立公式用 `$$...$$`,但用户问题不涉及数学,所以跳过。 - LaTeX语法正确:同样,不适用。 - 使用中文回答:整个回答要用中文。 - 生成相关问题:在回答结束后,添加相关问题部分。 - 回答中引用的段落末尾自然地添加引用标识,如 `[^1]`。引用来自提供的参考。 - 回答结构清晰,帮助用户逐步解决问题,保证真实可靠。 用户提供的引用: - [^1]: 关于闭包的实际应用:封装私有变量、延迟执行、保存状态。 - [^2]: 闭包的核心特点:记住词法作用域。 - [^3]: 常见闭包示例:函数作为返回值、作为实参传递、高阶函数与柯里化。 - [^4]: 闭包的影响和实例:定时器问题。 - [^5]: 另一个示例:事件处理中的闭包。 用户问题: "我想了解JavaScript闭包的常见示例 请问JavaScript 闭包常用示例及用法" 我需要基于这些引用构建回答。参考是站内引用,我应该自然地融入回答中。 回答结构: 1. **介绍闭包**:简要解释闭包是什么,基于引用[^2]。 2. **常见示例**:详细描述常见示例,从引用[^3]和[^5]。 - 函数作为返回值(计数器示例)。 - 函数作为实参传递(setTimeout示例)。 - 事件处理(按钮点击示例)。 - 定时器问题(引用[^4]的for循环问题)。 3. **用法**:讨论闭包的用途,基于引用[^1],如封装私有变量、延迟执行、保存状态。 4. **注意事项**:提到闭包可能的影响,如性能问题(引用[^4])。 5. **结尾**:总结并添加相关问题。 确保回答清晰、逐步解释。使用中文。 在引用段落末尾添加引用标识,例如:在描述一个概念后加 `[^1]`。 生成相关问题: - 基于主题,如闭包的优缺点、如何避免内存泄漏、实际应用场景等。 大纲回答: - 开头:定义闭包。 - 主体:常见示例和用法。 - 示例1:计数器(函数返回)。 - 示例2:setTimeout。 - 示例3:事件处理。 - 示例4:定时器问题。 - 用法:封装、延迟、状态保存。 - 注意事项:性能影响。 - 结尾:总结。 - 相关问题:3-5个问题。 现在,整合引用: - 当提到封装私有变量时,引用[^1]。 - 当描述核心特点时,引用[^2]。 - 当给出示例时,引用[^3]、[^4]、[^5]。 最终输出格式: - 回答部分。 - 相关问题部分以 "§§相关问题§§" 开头。</think>### JavaScript 闭包常用示例及用法详解 在 JavaScript 中,闭包(Closure)是一个核心概念,它允许函数访问并记住其定义时的词法作用域(Lexical Scope),即使函数在其作用域外部执行。闭包的核心特点包括:函数能“捕获”外部变量,并保持对这些变量的引用,从而在函数执行时访问或修改它们[^2]。闭包JavaScript 中有广泛的实际应用,例如封装私有变量、实现延迟执行或保存状态。下面我将逐步解释闭包的常见示例和用法,帮助您深入理解。 #### 一、闭包的常见示例 闭包的常见应用场景可以通过具体代码示例来展示。这些示例基于实际开发中的高频用例,确保您能直观掌握闭包的运作机制。 1. **函数作为返回值(计数器示例)** 这是闭包最经典的用法之一,常用于创建私有变量或状态管理。例如,定义一个函数返回另一个函数,内部函数访问并修改外部函数的变量: ```javascript function createCounter() { let count = 0; // 私有变量,通过闭包访问 return function() { count++; console.log(count); }; } const counter = createCounter(); counter(); // 输出: 1 counter(); // 输出: 2 ``` 在这个示例中,`createCounter` 函数返回的匿名函数形成了一个闭包,它“记住”了 `count` 变量,每次调用都会递增并输出其值。这实现了数据的封装和状态保存[^3]。 2. **函数作为实参传递(延迟执行示例)** 闭包常用于异步操作,如 `setTimeout` 或事件处理,以保存函数执行时的上下文。例如,传递一个闭包作为回调函数: ```javascript function showMessage(msg, delay) { setTimeout(function() { console.log(msg); // 闭包捕获了 msg 参数 }, delay); } showMessage('Hello, World!', 2000); // 2秒后输出 "Hello, World!" ``` 这里,匿名函数作为 `setTimeout` 的回调,通过闭包访问了 `msg` 参数,即使 `showMessage` 函数已执行完毕。这展示了闭包如何实现延迟执行和上下文保存[^3]。 3. **事件处理(按钮点击示例)** 在 DOM 操作中,闭包常用于事件监听器,以绑定特定数据到事件回调。例如,为多个按钮添加点击事件: ```javascript function setupButtons() { for (let i = 0; i < 3; i++) { document.getElementById(`btn${i}`).addEventListener('click', function() { console.log(`Button ${i} clicked`); // 闭包捕获了循环变量 i }); } } setupButtons(); // 每个按钮点击时输出对应索引 ``` 使用 `let` 声明循环变量 `i`,确保每个闭包捕获独立的 `i` 值。如果使用 `var`,会导致所有按钮输出相同值(如 `Button 3 clicked`),这是因为 `var` 的作用域问题。闭包在这里帮助保存了事件触发时的状态[^5]。 4. **定时器中的常见问题(循环闭包示例)** 闭包在循环中可能引发意外行为,需特别注意作用域管理。例如,一个经典的定时器问题: ```javascript for (var i = 1; i <= 3; i++) { setTimeout(function() { console.log(i); // 输出: 4, 4, 4(而非预期的 1,2,3) }, 1000); } ``` 问题原因:`var i` 是全局作用域,所有闭包共享同一个 `i`(循环结束后 `i=4`)。解决方案是使用 `let` 或创建新作用域: ```javascript for (let i = 1; i <= 3; i++) { // 使用 let 创建块级作用域 setTimeout(function() { console.log(i); // 输出: 1, 2, 3 }, 1000); } ``` 这突显了闭包在异步代码中的陷阱,需合理控制变量作用域[^4]。 #### 二、闭包的常见用法 闭包不仅用于示例场景,还广泛应用于以下功能,提升代码模块化和可维护性[^1]: - **封装私有变量和方法**:闭包可以创建模块化组件,避免全局污染。例如,实现一个简单的“私有”计数器: ```javascript const counterModule = (function() { let privateCount = 0; return { increment: function() { privateCount++; }, getCount: function() { return privateCount; } }; })(); counterModule.increment(); console.log(counterModule.getCount()); // 输出: 1 ``` 这里,IIFE(立即执行函数)返回的对象方法通过闭包访问 `privateCount`,外部无法直接修改该变量[^1]。 - **延迟函数执行**:闭包保存参数和上下文,用于定时器或事件队列。例如,在 AJAX 回调中保持数据状态。 - **保存状态**:闭包使函数能“记住”先前操作,如实现状态机或缓存机制。例如,记忆化(Memoization)优化递归计算: ```javascript function memoize(fn) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (cache[key]) return cache[key]; const result = fn.apply(this, args); cache[key] = result; return result; }; } const factorial = memoize(n => n <= 1 ? 1 : n * factorial(n-1)); console.log(factorial(5)); // 计算并缓存结果 ``` #### 三、注意事项 尽管闭包功能强大,但需注意潜在问题: - **性能影响**:频繁创建闭包可能增加内存开销,因为每个闭包都捕获整个作用域链。在性能敏感场景(如高频事件),应避免不必要的闭包创建[^4]。 - **内存泄漏风险**:如果闭包引用 DOM 元素或其他大对象,可能导致垃圾回收器无法释放内存。解决方法是手动解除引用(如移除事件监听器)。 - **作用域管理**:确保正确使用 `let` 或 `const` 避免变量共享问题,如前述循环示例。 #### 总结 闭包JavaScript 的核心特性,通过示例(如计数器、事件处理和定时器)和用法(封装、延迟执行、状态保存),它实现了强大的模块化和异步处理能力。理解闭包的工作原理(词法作用域捕获)和注意事项(性能优化),能帮助您编写更健壮的代码。在实际开发中,闭包常用于框架(如 React Hooks)和库设计,提升代码复用性[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值