鲁斯前端布鲁斯前端

文章中英模式

布鲁斯前端JS面试题目 - JavaScript 闭包(Closure)

从基础理解 JavaScript 闭包(Closure)与 Lexical Scope,并搭配 useState 模型理解 React 如何记住 state,搭配内存泄漏案例与常见面试题,全面掌握闭包原理与陷阱。

影片縮圖

懒得看文章?那就来看视频吧

什么是闭包?JavaScript 为什么需要它?

闭包(Closure)是指一个函数和它定义时的外部变量的组合。即使函数被传递到不同的作用域执行,它仍然可以存取当时建立时的变量。

  • 保存函数中的中间状态(例如计数器)
  • 封装数据,实现私有变量
  • 提供记忆性(如 memoization)
  • 处理异步流程与事件回调
function createCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

词法作用域(Lexical Scope)是什么?和闭包有什么关系?

JavaScript 采用词法作用域(Lexical Scope),也就是「函数可以存取的是它定义当下的变量环境,而不是它执行当下的环境」。

闭包就是这个机制的延伸,让函数在未来仍然能记得定义当下的变量。

function outer() {
  const name = 'Bruce';
  function greet() {
    console.log('Hello, ' + name);
  }
  return greet;
}

const sayHello = outer();
sayHello(); // Hello, Bruce

内存泄漏的真相:闭包用不好就变成地雷

闭包延长变量生命周期,若引用大型数据或DOM元素,又与长寿命结构绑定,会导致内存无法释放。

范例 1:全局变量与 DOM 结合

let cache = {};

function bindElement(elementId) {
  const element = document.getElementById(elementId);
  cache[elementId] = () => {
    console.log(element.innerText);
  };
}

bindElement('myDiv');

DOM元素被闭包引用并存在全局cache中,即使元素被移除,GC也无法回收。解决:不需要时设置 cache[elementId] = null。

范例 2:setTimeout 长期引用变量

function setupLogger() {
  let bigData = new Array(1000000).fill('*');
  setTimeout(() => {
    console.log(bigData[0]);
  }, 10000);
}

计时器回调引用大型数组,即使函数执行完毕,bigData仍保留在内存10秒。问题在于闭包让setTimeout持续引用bigData,阻止垃圾回收。解决:清除计时器或只保留必要数据。

React 的 useState 为什么能记住值?多次调用 Hook 为什么需要闭包?

React 在每次 render 时会重新执行整个组件函数。但 useState 返回的 getter/setter 是闭包,能记住对应状态。

function simpleUseState(initialValue) {
  let value = initialValue;
  return [
    () => value,
    (newVal) => { value = newVal; }
  ];
}

const [getA, setA] = simpleUseState(1);
const [getB, setB] = simpleUseState(100);

setA(2);
console.log(getA()); // 2
console.log(getB()); // 100

🔥 常见面试题目

(一)什么是闭包?JavaScript 为什么需要它?

闭包是函数与其变量环境的组合,让函数记住定义时的变量。JavaScript 用它来保存状态、封装数据和处理回调。

function createCounter() {
  let count = 0;  // 这个变量被闭包保存
  
  return function() {
    count += 1;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

(二)词法作用域是什么?与闭包的关系?

词法作用域指变量范围由定义位置决定,非执行位置。闭包基于此特性,让函数带着「出生环境」到处执行。

let name = 'Global';

function outer() {
  let name = 'Outer';
  
  function inner() {
    console.log(name); // 使用的是 outer 的 name
  }
  
  return inner;
}

const innerFn = outer();
innerFn(); // 'Outer' - 即使在全局执行,仍记得定义时的环境

(三)如何用闭包实现私有变量?

闭包可创建私有变量,外部只能通过指定方法存取,实现类似面向对象的封装概念。

function createSecret() {
  let secret = '1234';
  return {
    get() { return secret; },
    set(val) { secret = val; }
  };
}

const user = createSecret();
console.log(user.get()); // '1234'
user.set('abcd');
console.log(user.get()); // 'abcd'
// console.log(secret); // 错误!外部无法直接存取

(四)React 的 useState 是怎么利用闭包来记住值?多次调用 Hook 为什么需要闭包?

每个 useState 创建独立闭包,为每个状态提供专属「保险箱」,使状态在重新渲染时保持独立且不互相干扰。

function simpleUseState(initialValue) {
  let value = initialValue;
  return [
    () => value,
    (newVal) => { value = newVal; }
  ];
}

const [getA, setA] = simpleUseState(1);
const [getB, setB] = simpleUseState(100);

setA(2);
console.log(getA()); // 2
console.log(getB()); // 100

(五)闭包可能造成内存泄漏的情境有哪些?

闭包持续引用大型数据或DOM元素且未释放时,会阻止垃圾回收,像忘关水龙头般消耗资源。

  • 全局变量绑定 DOM(造成元素无法被 GC)
  • setTimeout 或事件监听绑定变量未清除
// 内存泄漏范例
function setupEventHandler() {
  const largeData = new Array(10000).fill('data');
  
  document.getElementById('button').addEventListener('click', function() {
    console.log(largeData.length); // 持续引用大型数据
  });
  
  // 正确做法:保存引用并在适当时机移除
  // const handler = function() { console.log(largeData.length); };
  // document.getElementById('button').addEventListener('click', handler);
  // 之后可以用 removeEventListener('click', handler) 移除
}

(六)工厂函数与闭包的关联是什么?

工厂函数利用闭包批量生产功能相似但参数不同的函数,每个函数都记住自己的独立环境。

function createMultiplier(factor) {
  return function(num) {
    return num * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15