【前端面试知识点】JavaScript:什么是闭包?闭包可能导致哪些问题?如何避免内存泄漏?

13 阅读4分钟

什么是闭包?

闭包 是指一个函数能够“记住”并访问其定义时的词法作用域,即使这个函数在其定义的作用域之外被调用。简单来说,闭包就是函数与其外部环境引用的组合。

产生条件

  • 函数嵌套函数
  • 内部函数引用了外部函数的变量
  • 内部函数被“保存”到外部,例如作为返回值或传递给其他函数

javascript

function outer() {
  let name = "inner";
  function inner() {
    console.log(name); // 访问外部变量
  }
  return inner; // 将内部函数返回,形成闭包
}
const closure = outer();
closure(); // 输出 "inner",尽管 outer 已执行完毕,name 依然被保留

闭包可能导致的问题

1. 内存泄漏

闭包会使得外部函数的变量持续被引用,即使外部函数已执行完毕,这些变量也无法被垃圾回收。如果闭包长期存在(例如作为事件监听、定时器回调),这些变量就会一直占用内存,造成内存泄漏。

javascript

function createHeavyData() {
  const bigData = new Array(1000000).fill('data');
  return function() {
    console.log(bigData.length); // 闭包引用了 bigData
  };
}
const leak = createHeavyData(); // bigData 无法被回收
// 即使不再使用 leak,若未释放引用,内存不会回收

2. 意外共享变量

在循环中创建闭包时,如果使用 var 声明变量,所有闭包共享同一个变量,导致最终值相同。

javascript

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(而非 0,1,2

3. 性能开销

不必要的闭包会增加内存占用和函数调用链的复杂度,尤其在大量创建时可能影响性能。

4. DOM 循环引用

如果闭包中引用了 DOM 元素,而 DOM 元素又通过事件等引用了闭包,可能形成循环引用,导致内存无法释放(虽然现代浏览器已能处理部分情况,但仍需注意)。

javascript

function bindClick() {
  const element = document.getElementById('btn');
  element.onclick = function() {
    console.log(element.id); // 闭包引用 element
  };
}
bindClick();
// 即使页面移除 btn,闭包仍持有 element 引用,可能造成泄漏

如何避免闭包导致的内存泄漏?

1. 及时释放闭包引用

当不再需要闭包时,将其引用置为 null,以便垃圾回收器回收。

javascript

let leak = createHeavyData();
// 使用完毕后
leak = null; // 解除引用,等待回收

2. 避免不必要的闭包

只在确实需要访问外部变量时才使用闭包;如果不需要,就不要在函数内返回函数。

3. 使用 let 或 const 替代 var

在循环中,使用 let 会为每次迭代创建新的块级作用域,避免变量共享问题。

javascript

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出:0,1,2

4. 在 DOM 事件中及时解绑

移除 DOM 元素前,将事件监听置空或使用 removeEventListener,切断闭包引用。

javascript

function bindClick() {
  const element = document.getElementById('btn');
  const handler = () => console.log(element.id);
  element.addEventListener('click', handler);
  // 当需要清理时:
  // element.removeEventListener('click', handler);
  // element = null;
}

5. 使用弱引用(WeakMap、WeakSet)

如果闭包需要缓存一些对象,但又不想阻止其垃圾回收,可以使用 WeakMap 或 WeakSet 存储引用。

javascript

const cache = new WeakMap();
function process(obj) {
  if (!cache.has(obj)) {
    cache.set(obj, heavyCalculation(obj));
  }
  return cache.get(obj);
}
// obj 被垃圾回收时,缓存中的对应条目也会自动清除

6. 避免无意中创建大对象闭包

在闭包中只引用必要的变量,而非整个外部对象。可以使用参数传递代替直接引用。

javascript

// 不好
function outer() {
  const huge = new Array(1000000);
  return function() {
    console.log(huge.length); // 持有整个数组
  };
}

// 好
function outer() {
  const huge = new Array(1000000);
  const length = huge.length; // 只提取需要的数据
  return function() {
    console.log(length);
  };
}

7. 利用垃圾回收机制

现代 JavaScript 引擎(如 V8)在闭包中只保留被实际引用的变量,而不是整个词法环境。尽管如此,开发者仍应遵循上述最佳实践。


总结

  • 闭包是 JavaScript 的核心特性,广泛应用于模块化、函数柯里化等场景。
  • 不合理的闭包使用可能导致内存泄漏、性能下降和意外行为。
  • 通过及时解除引用、使用块级作用域、弱引用以及避免过度闭包,可以有效规避问题,写出高效、健壮的代码。