闭包

闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的信息可以在函数中使用。换句话说,执行函数时,只要在函数中使用了外部的数据,就创建了闭包

实际用处:解决全局变量污染,手动创建的闭包用null销毁(解除引用)

闭包最核心的作用是:突破函数作用域的限制,让函数能 “记住” 并操作它定义时所在的外部环境中的变量,即使外部环境已经消失

那么多闭包,那岂不是占用内存空间么?
实际上,如果是自动形成的闭包,是会被销毁掉的

  • 闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。

  • 只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。

  • 我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。

// 例子
for (var i = 0; i < 5; i++) {
    setTimeout(function () { // 访问外层作用域的变量i(全局的),产生闭包
        console.log(i)
    }, 1000)
}
// 5 5 5 5:异步,循环结束才输出,输出时,找变量i,找到外层作用域的 i只有一个 ,此时i为5

// 闭包解决
for (var i = 0; i < 5; i++) {
    (function (index) {
        setTimeout(function () {
            console.log(index) // 访问外层作用域的变量index(IIFE的局部变量),产生闭包
        }, 1000)
    })(i);
}
// ES6 的 let解决
for (let i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log(i) // 每个回调都产生闭包,访问外层作用域的变量i(let的块级作用域)
    }, 1000)
}

补充

一、什么是闭包?

闭包是 JavaScript 中函数与其词法环境的组合——函数创建时会捕获其所处的词法作用域(定义时的作用域,而非执行时的作用域),即使函数在创建时的作用域之外执行,也能访问该作用域内的变量、参数和其他函数。

核心本质:

  • 词法作用域是闭包的基础(函数的作用域由定义位置决定,而非调用位置);

  • 闭包的核心是「函数+创建时的词法环境引用」,让函数突破定义时的作用域限制。

极简示例:

function outer() {
  // 外部函数的局部变量(词法环境中的变量)
  let count = 0;
  
  // 内部函数(闭包):捕获 outer 的词法环境
  function inner() {
    count++; // 访问外部函数的局部变量
    console.log(count);
  }
  
  return inner; // 外部函数返回内部函数(突破作用域限制)
}

// 调用 outer 后,inner 被赋值给 add,此时 outer 已执行完毕
const add = outer(); 

add(); // 1(inner 仍能访问 outer 的 count 变量)
add(); // 2(count 未被销毁,生命周期被延长)

二、闭包解决了什么问题?

闭包的核心价值是突破作用域限制,解决了 JavaScript 中「变量访问范围」和「生命周期」的关键问题,主要应用场景有 3 类:

1. 实现变量私有化,避免全局污染

JavaScript 早期没有「块级作用域」(ES6 前只有函数作用域和全局作用域),全局变量容易被篡改,闭包可以模拟「私有变量」,让变量仅能通过指定接口访问。

示例(模块化封装私有变量):

const counterModule = (function() {
  // 私有变量(外部无法直接访问)
  let privateCount = 0;
  
  // 私有方法(仅内部可用)
  function privateLog(msg) {
    console.log(`${msg}: ${privateCount}`);
  }
  
  // 暴露公共接口(闭包:访问私有变量/方法)
  return {
    increment: function() {
      privateCount++;
    },
    log: function() {
      privateLog("当前计数"); // 1 → 2 → ...
    }
  };
})();

counterModule.increment();
counterModule.log(); // 当前计数: 1
console.log(counterModule.privateCount); // undefined(无法直接访问私有变量)

2. 延长变量的生命周期

函数执行完毕后,其作用域通常会被垃圾回收机制(GC)销毁,但闭包会引用该作用域的变量,导致作用域无法销毁,变量生命周期被延长。

示例(定时器/回调中访问局部变量):

function getTimer() {
  let seconds = 0;
  
  // 定时器回调是闭包,捕获 seconds
  setInterval(function() {
    seconds++;
    console.log(`已运行 ${seconds} 秒`);
  }, 1000);
}

getTimer(); // 每隔 1 秒输出,seconds 持续累加(未被 GC 回收)

3. 实现模块化(早期无 ES6 Module 时)

闭包是 JavaScript 早期实现模块化的核心方案(如 CommonJS 早期原理),通过「私有作用域+暴露公共接口」,将代码分割为独立模块,避免命名冲突。

示例(工具函数模块):

const mathTool = (function() {
  const PI = 3.14159; // 私有常量
  
  return {
    circleArea: function(r) {
      return PI * r * r; // 闭包访问 PI
    },
    circleCircumference: function(r) {
      return 2 * PI * r;
    }
  };
})();

console.log(mathTool.circleArea(2)); // 12.56636
console.log(mathTool.PI); // undefined(私有变量不可访问)

三、闭包带来什么问题?

闭包的「延长变量生命周期」特性是把双刃剑,滥用会引发以下问题:

1. 内存泄漏(最核心问题)

闭包引用的词法环境(含变量、参数)会被持续持有,即使这些变量不再需要,也无法被 GC 回收,导致内存占用过高,长期积累可能引发页面卡顿、崩溃。

示例(不当闭包导致内存泄漏):

function createBigClosure() {
  // 模拟大数据(100万条数据,占用大量内存)
  const largeData = new Array(1000000).fill('large-data');
  
  // 闭包引用 largeData,即使外部不需要,也无法回收
  return function() {
    console.log(largeData.length);
  };
}

const unusedClosure = createBigClosure();
// 即使 unusedClosure 不再使用,largeData 仍被引用,无法 GC
// 解决:不再使用时手动释放引用 → unusedClosure = null;

闭包导致内存泄漏详细解释解析:

  1. 持有了不在需要的函数引用,会导致函数关联的词法环境无法销毁

  2. 当多个函数共享词法环境,会导致词法环境膨胀,从而导致出现无法触达也无法回收的内存空间

2. 变量共享陷阱(循环中常见)

若多个闭包共享同一个词法环境的变量,可能导致意外的变量覆盖,典型场景是「循环中创建闭包」(ES6 前 var 无块级作用域时)。

示例(循环闭包的变量共享问题):

// 期望输出 0、1、2,实际输出 3、3、3
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 所有闭包共享同一个 i(循环结束后 i=3)
  }, 1000);
}

// 解决方案:
// 1. ES6 用 let 创建块级作用域(每个循环迭代有独立 i)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000); // 0、1、2
}
// 2. ES5 用 IIFE 传参(创建独立作用域)
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 1000); // 0、1、2
  })(i);
}

3. 调试复杂度提升

闭包的作用域链嵌套较深,变量的来源不直观(可能来自外层函数、外层的外层函数),调试时难以追踪变量的赋值和修改路径,增加排查问题的难度。

4. 性能损耗

闭包需要维护额外的词法环境引用,函数执行时会多一层作用域链查找(而非直接访问当前作用域变量),频繁创建或调用闭包可能带来轻微的性能开销(通常不明显,滥用时才会凸显)。

四、总结

  • 闭包定义:函数+创建时的词法环境引用,让函数在定义作用域外仍能访问内部变量;

  • 解决的问题:变量私有化、延长变量生命周期、实现模块化;

  • 带来的问题:内存泄漏、变量共享陷阱、调试困难、轻微性能损耗;

  • 使用建议:合理使用闭包(如模块化、私有变量场景),避免滥用;不再使用时手动释放闭包引用(closure = null),减少内存泄漏风险。