js中的闭包

本文详细解释了JavaScript中闭包的概念及其工作原理。通过具体示例介绍了闭包如何允许内部函数访问外部函数的变量,并探讨了闭包在函数执行过程中的作用域链机制。

原文链接—-请点这里

  闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数。
  之所以一个内部的函数可以访问其外部的变量,而且在其被返回或是调用时还可以访问,是因为这个内部函数的作用域链中包含外部函数的作用域。

知识储备

在了解闭包之前,先要熟悉以下几点:
  1. 首先要理解执行环境,执行环境定义了变量或函数有权访问的其他数据。
  2. 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
  3. 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
  4. 当某个函数被调用时,会创建一个执行环境及其相应的作用域链。然后使用arguments和其他命名参数的值来初始化函数的活动对象。在函数中,活动对象作为变量对象使用(作用域链是由每层的变量对象链起来的)。
  5. 在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,直到作用域链终点即全局执行环境。
  6. 作用域链的本质是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。


1.一般情况下

  不谈论闭包,一般的,从在全局执行环境创建一个函数开始。
  在创建一个函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数内部的[[Scope]]
  然后执行流进入这个函数,函数的执行环境被压入环境栈中,此函数执行环境的活动对象作为变量对象被创建并推入执行环境作用域链的前端。
  对这个例子中的函数而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。
  无论在什么时候在函数中访问变量时,会从作用域链搜索变量名。
  一般情况下,函数执行完,局部活动对象就会被销毁,内存中仅有全局作用域(里边只有全局执行环境的变量对象)。
  以下面这段代码为例:

function compare (value1, value2) {         
//创建一个预先包含全局变量对象的作用域链,保存在[[Scope]]
    if (value1 < value2) {
//访问函数变量时,即在代码最后一条语句执行过程中,会从作用域链前端开始搜索变量名
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
var result = compare(5, 10);
//执行流进入函数时,compare的执行环境压入环境栈
//compare执行环境的活动对象作为变量对象接到作用域链的前端
//函数执行完,compare执行环境弹出栈,compare活动对象销毁

如图,作用域链从0开始向后查找:
闭包1.1


2.产生闭包的情况下

如下是一个以属性名作为参数,按其属性的值对数据进行排序的函数:

function createComparisonFunction(propertyName) {     
    return function(object1,object2){         //返回一个匿名函数
        var value1=object1[propertyName];
        var value2=object2[propertyName];
        if(value1<value2){
            return -1;
        } else if (value1>value2){
            return 1;
        } else {
            return 0;
        }
    };
}
var data=[{name:"Zachary",age:28},{name:"Nicholas",age:29}];
data.sort(createComparisonFunction("name"));
console.log(data[0]);           //Object {name: "Nicholas", age: 29}
data.sort(createComparisonFunction("age"));
console.log(data[0]);           //Object {name: "Zachary", age: 28}

createComparisonFunction()函数和返回的匿名函数的作用域链如下图所示:
闭包2.1

  在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。

更为重要的是:

  createComparisonFunction()函数在执行完毕后,其他活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。
  当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁,createComparisonFunction()的活动对象才会被销毁。

  例如以下代码,返回的匿名函数被保存在变量compareNames中,通过将compareNames设置为null来解除对匿名函数的引用,解除引用之后垃圾回收例程将会清除该匿名函数,随之该匿名函数的作用域链也会被销毁,则其作用域链上的其他作用域也会安全的销毁(全局作用域除外)。

var compareNames = createComparisonFunction("name");
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
compareNames = null;
<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、付费专栏及课程。

余额充值