文章目录

JavaScript 闭包:函数作用域与变量访问

发布于 2026-04-11 20:22:29 · 浏览 8 次 · 评论 0 条

JavaScript 闭包:函数作用域与变量访问

闭包是 JavaScript 中最核心、最强大的特性之一。简单来说,闭包允许函数“记住”并访问其定义所在作用域的变量,即使该函数在其原始作用域之外执行。这就好比函数出门时背了一个背包,背包里装着它出生时的环境变量。

以下将通过具体步骤,深入解析闭包的形成原理、实际应用及常见陷阱。


第一步:理解词法作用域

在创建闭包之前,必须先理解 JavaScript 的“词法作用域”。这意味着函数在定义时(而不是调用时)决定了它能访问哪些变量。

  1. 打开浏览器的开发者工具(按 F12),切换Console(控制台)面板。
  2. 输入以下代码并 Enter 执行。
function outerFunction() {
    var outerVar = "我是外部变量";

    function innerFunction() {
        console.log(outerVar);
    }

    return innerFunction;
}

在这段代码中,innerFunction 定义在 outerFunction 内部。根据词法作用域规则,innerFunction 可以访问 outerVar。此时,闭包尚未真正发挥作用,只是简单的嵌套访问。


第二步:触发闭包机制

闭包的特殊之处在于,当内部函数被传递到其定义的作用域之外执行时,它依然能访问原始作用域的变量。

  1. 继续在控制台输入以下代码:
const myClosure = outerFunction();

这一步发生了关键过程:

  • outerFunction() 执行
  • 变量 outerVar 被创建。
  • innerFunction 被定义并返回
  • outerFunction 执行结束,通常情况下其内部的局部变量 outerVar 会被销毁(垃圾回收)。
  • 但是,因为返回的 innerFunction 引用了 outerVar,JavaScript 引擎保留outerVar 的内存空间。
  1. 输入执行这行代码:
myClosure();

控制台输出 我是外部变量

结论outerFunction 已经执行完毕,但 myClosure(即 innerFunction)依然“记得” outerVar 的值。这就是闭包。

为了更直观地理解这一过程,请看下面的执行流程图:

graph LR A["定义 outerFunction"] --> B["调用 outerFunction()"] B --> C["创建局部变量 outerVar"] C --> D["定义 innerFunction\n(引用了 outerVar)"] D --> E["返回 innerFunction"] E --> F["outerFunction 执行栈弹出\n(通常变量会被销毁)"] F --> G["闭包生效:\nouterVar 被保留在内存中"] G --> H["调用 myClosure()\n(即 innerFunction)"] H --> I["成功访问 outerVar"]

第三步:利用闭包实现数据私有化

闭包最常见的用途是创建私有变量。在 JavaScript 中,没有原生的 private 关键字(在 ES6 类语法普及前),但可以通过闭包模拟。

  1. 输入以下代码,创建一个简单的计数器:
function createCounter() {
    let count = 0; // 这个变量是私有的,外部无法直接修改

    return {
        increment: function() {
            count++;
            console.log(count);
        },
        decrement: function() {
            count--;
            console.log(count);
        },
        getCount: function() {
            return count;
        }
    };
}
  1. 实例化一个计数器对象:
const counterA = createCounter();
  1. 尝试直接访问 count 变量:
console.log(counterA.count);

输出为 undefined,因为 count 被封闭在 createCounter 作用域内,外部对象无法直接触碰。

  1. 调用提供的方法来操作变量:
counterA.increment(); // 输出 1
counterA.increment(); // 输出 2
counterA.decrement(); // 输出 1

这就实现了一个安全的数据封装,防止外部代码随意篡改内部状态。


第四步:使用闭包工厂(函数工厂)

闭包可以用来定制函数。通过传递参数给外部函数,可以让内部函数记住这些预设参数。

  1. 编写一个乘法工厂函数:
function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}
  1. 创建两个具体的乘法函数:
const double = createMultiplier(2);
const triple = createMultiplier(3);
  1. 执行观察结果:
函数名 调用代码 计算逻辑 (闭包保存) 结果
double double(5) 5 * 2 10
triple triple(5) 5 * 3 15

控制台验证

console.log(double(5)); // 输出 10
console.log(triple(5)); // 输出 15

在这个过程中,double 函数“记住”了 2triple 函数“记住”了 3


第五步:解决循环中的闭包陷阱

这是初学者最容易踩的坑。在循环中使用 var 声明变量,再配合闭包(如定时器或事件监听),往往会出现意想不到的结果。

  1. 观察下面一段有问题的代码:
for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

预期结果可能是 1, 2, 3。但实际输出是 4, 4, 4

原因分析

  • var 声明的 i 是函数作用域变量,在循环结束后,i 变成了 4
  • setTimeout 的回调函数在 1 秒后执行,此时它们通过闭包引用的是同一个 i,即 4
  1. 修复方法一:使用 let(推荐)。

    • var 替换let
for (let i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

let 具有块级作用域,每次循环都会创建一个新的 i,闭包会绑定当前块内的 i。输出将正确显示 1, 2, 3

  1. 修复方法二:使用 IIFE(立即执行函数表达式)。

如果在老代码中必须支持 var,可以使用 IIFE 创建一个新的作用域来保存当前的 i 值。

for (var i = 1; i <= 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 1000);
    })(i);
}

这里,外层的匿名函数立即执行,并将当前的 i 值作为参数 j 传入。内部的闭包引用的是参数 j,它是每次循环独立复制的副本。


第六步:管理与释放内存

由于闭包会引用外部函数的变量,导致这些变量无法被垃圾回收机制回收,如果滥用闭包,可能导致内存泄漏(Memory Leak)。

  1. 识别潜在的泄漏风险。如果创建了大量闭包且不再使用,它们占用的内存将持续存在。
  2. 手动解除引用。当确认闭包不再需要时,引用它的变量设为 null
const bigClosure = (function() {
    const bigData = new Array(1000000).fill("data");
    return function() {
        console.log(bigData.length);
    };
})();

// 使用完毕后
bigClosure(); // 执行一次
bigClosure = null; // 解除引用,允许垃圾回收 bigData

按下 Ctrl + S 保存代码逻辑,并在实际项目中养成监控内存的习惯。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文