Skip to content

深入理解闭包与内存

从执行上下文到作用域链,彻底搞懂闭包的形成机制、应用场景和内存影响

什么是闭包?

定义:闭包(Closure)是指一个函数能够记住并访问其词法作用域,即使这个函数在其词法作用域之外执行。简单说,当内部函数引用了外部函数的变量,且内部函数在外部函数执行完毕后仍然存活时,就形成了闭包。

涉及场景

  • 数据封装:模拟私有变量,外部无法直接访问内部状态
  • 函数工厂:根据参数动态生成具有特定行为的函数
  • 回调与事件处理:事件监听器、定时器回调天然形成闭包
  • 柯里化与偏函数:利用闭包保存部分参数,生成新函数
  • 模块模式:IIFE + 闭包实现模块化(ES Module 之前的主流方案)
  • React HooksuseStateuseEffect 内部依赖闭包捕获状态

作用

  1. 状态持久化:函数执行完毕后,被引用的变量不会被垃圾回收
  2. 信息隐藏:实现类似面向对象的私有属性和方法
  3. 函数式编程基石:高阶函数、柯里化、组合都依赖闭包
  4. 常见陷阱来源:循环中的闭包、内存泄漏是高频面试考点

前置知识:执行上下文

JavaScript 代码执行时,引擎会创建执行上下文(Execution Context)来管理代码的运行环境。

执行上下文的类型

  1. 全局执行上下文 — 程序启动时创建,只有一个
  2. 函数执行上下文 — 每次调用函数时创建
  3. eval执行上下文 — eval() 中的代码(不推荐使用)

执行上下文栈(Call Stack)

javascript
function first() {
  console.log('first');
  second();
  console.log('first end');
}

function second() {
  console.log('second');
  third();
  console.log('second end');
}

function third() {
  console.log('third');
}

first();

// 调用栈变化:
// ┌─────────┐
// │  third   │ ← 栈顶(当前执行)
// ├─────────┤
// │  second  │
// ├─────────┤
// │  first   │
// ├─────────┤
// │  global  │ ← 栈底
// └─────────┘
// third 执行完 → 弹出
// second 继续 → "second end" → 弹出
// first 继续 → "first end" → 弹出

执行上下文的组成(ES2015+)

javascript
ExecutionContext = {
  // 1. 词法环境(LexicalEnvironment)
  LexicalEnvironment: {
    EnvironmentRecord: {
      // let、const、函数声明存储在这里
    },
    OuterReference: null // 指向外部词法环境(作用域链的关键)
  },

  // 2. 变量环境(VariableEnvironment)
  VariableEnvironment: {
    EnvironmentRecord: {
      // var 声明存储在这里
    },
    OuterReference: null
  },

  // 3. this 绑定
  ThisBinding: undefined
}

作用域与作用域链

词法作用域(静态作用域)

JavaScript 采用词法作用域——函数的作用域在定义时就确定了,而不是在调用时。

javascript
const x = 10;

function foo() {
  console.log(x); // 10(查找定义时的外部作用域)
}

function bar() {
  const x = 20;
  foo(); // 输出 10,不是 20!
}

bar();
// foo 在定义时,外部作用域是全局,所以 x = 10
// 即使在 bar 中调用,也不会使用 bar 的 x

作用域链的形成

javascript
const global = 'global';

function outer() {
  const outerVar = 'outer';

  function middle() {
    const middleVar = 'middle';

    function inner() {
      const innerVar = 'inner';
      // 作用域链:inner → middle → outer → global
      console.log(innerVar);   // 'inner'(自身)
      console.log(middleVar);  // 'middle'(middle的)
      console.log(outerVar);   // 'outer'(outer的)
      console.log(global);     // 'global'(全局的)
    }

    inner();
  }

  middle();
}

outer();

// 作用域链示意:
// inner.[[Scope]] = [innerEnv, middleEnv, outerEnv, globalEnv]
// 查找变量时从左到右依次查找

闭包的定义

闭包 = 函数 + 函数定义时的词法环境

当一个函数能够访问其外部函数的变量,即使外部函数已经执行完毕(执行上下文已出栈),这就形成了闭包。

javascript
function createCounter() {
  let count = 0; // 自由变量

  return function increment() {
    count++;
    return count;
  };
  // createCounter 执行完毕后,count 本应被销毁
  // 但 increment 函数引用了 count → 闭包形成
  // count 被保留在内存中
}

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

// counter 持有对 increment 的引用
// increment 的 [[Scope]] 中持有 createCounter 的词法环境
// 该词法环境中有 count 变量
// 因此 count 不会被垃圾回收

闭包的本质

javascript
function outer() {
  const a = 1;
  const b = 2;  // b 没有被内部函数引用

  return function inner() {
    return a; // 只引用了 a
  };
}

const fn = outer();

// V8引擎的优化:
// 闭包只保留被引用的变量(a),未被引用的变量(b)会被回收
// 这称为"闭包变量精简"
// 但使用 eval 时,引擎无法确定哪些变量被引用,会保留所有变量

闭包的经典应用

1. 数据封装(模拟私有变量)

javascript
function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有变量
  const transactions = [];      // 私有变量

  return {
    deposit(amount) {
      if (amount <= 0) throw new Error('金额必须大于0');
      balance += amount;
      transactions.push({ type: 'deposit', amount, date: new Date() });
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error('余额不足');
      balance -= amount;
      transactions.push({ type: 'withdraw', amount, date: new Date() });
      return balance;
    },
    getBalance() {
      return balance; // 只能通过方法访问
    },
    getTransactions() {
      return [...transactions]; // 返回副本,防止外部修改
    }
  };
}

const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
// account.balance → undefined(无法直接访问)

2. 函数工厂

javascript
function createMultiplier(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenTimes = createMultiplier(10);

console.log(double(5));   // 10
console.log(triple(5));   // 15
console.log(tenTimes(5)); // 50

// 每次调用 createMultiplier 都创建一个新的闭包
// 每个闭包有自己独立的 multiplier

3. 缓存(记忆化)

javascript
function memoize(fn) {
  const cache = new Map(); // 闭包中的缓存

  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log('cache hit');
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalculation = memoize((n) => {
  console.log('computing...');
  let result = 0;
  for (let i = 0; i < n; i++) result += Math.sqrt(i);
  return result;
});

expensiveCalculation(1000000); // 'computing...' → 计算
expensiveCalculation(1000000); // 'cache hit' → 直接返回

4. 防抖与节流

javascript
// 防抖(闭包保存 timer)
function debounce(fn, delay) {
  let timer = null; // 闭包变量

  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 节流(闭包保存上次执行时间)
function throttle(fn, interval) {
  let lastTime = 0; // 闭包变量

  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

5. 迭代器

javascript
function createIterator(array) {
  let index = 0; // 闭包保存迭代状态

  return {
    next() {
      if (index < array.length) {
        return { value: array[index++], done: false };
      }
      return { done: true };
    },
    reset() {
      index = 0;
    }
  };
}

const it = createIterator([10, 20, 30]);
console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: 30, done: false }
console.log(it.next()); // { done: true }

6. 单例模式

javascript
const createSingleton = (function() {
  let instance = null; // 闭包保存唯一实例

  return function(config) {
    if (!instance) {
      instance = {
        config,
        createdAt: new Date(),
        // ...
      };
    }
    return instance;
  };
})();

const a = createSingleton({ name: 'A' });
const b = createSingleton({ name: 'B' });
console.log(a === b); // true(同一个实例)

经典面试题

题目1:循环中的闭包

javascript
// 问题代码
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}
// 输出:5 5 5 5 5
// 原因:var 是函数作用域,5个回调共享同一个 i
// setTimeout 执行时,循环已结束,i = 5

// 解决方案1:IIFE 创建闭包
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, j * 1000);
  })(i); // 每次循环创建新的作用域,j 是 i 的副本
}
// 输出:0 1 2 3 4

// 解决方案2:let 块级作用域(推荐)
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}
// 输出:0 1 2 3 4
// let 在每次循环迭代中创建新的绑定

// 解决方案3:setTimeout 第三个参数
for (var i = 0; i < 5; i++) {
  setTimeout(function(j) {
    console.log(j);
  }, i * 1000, i); // 第三个参数传递给回调
}

题目2:闭包与this

javascript
const obj = {
  name: '张三',
  getName: function() {
    return function() {
      return this.name;
    };
  }
};

console.log(obj.getName()()); // undefined(this指向window/global)
// 内部函数是独立调用的,this不指向obj

// 修复方案1:箭头函数
const obj2 = {
  name: '张三',
  getName: function() {
    return () => this.name; // 箭头函数继承外部this
  }
};
console.log(obj2.getName()()); // '张三'

// 修复方案2:闭包保存this
const obj3 = {
  name: '张三',
  getName: function() {
    const self = this; // 闭包捕获this
    return function() {
      return self.name;
    };
  }
};
console.log(obj3.getName()()); // '张三'

题目3:闭包共享变量

javascript
function createFunctions() {
  const result = [];

  for (var i = 0; i < 3; i++) {
    result.push(function() {
      return i;
    });
  }

  return result;
}

const fns = createFunctions();
console.log(fns[0]()); // 3(不是0!)
console.log(fns[1]()); // 3
console.log(fns[2]()); // 3
// 三个函数共享同一个闭包中的 i,循环结束后 i = 3

题目4:闭包与垃圾回收

javascript
function heavyClosure() {
  const hugeData = new Array(1000000).fill('*'); // 大数据

  return function() {
    // 只用了 hugeData.length,但整个 hugeData 都被保留
    return hugeData.length;
  };
}

const fn = heavyClosure();
// hugeData 无法被回收,因为 fn 的闭包引用了它

// 优化:只保存需要的数据
function optimizedClosure() {
  const hugeData = new Array(1000000).fill('*');
  const length = hugeData.length; // 提取需要的值

  return function() {
    return length; // 只引用了 length
  };
  // hugeData 没有被闭包引用,可以被GC回收
}

闭包与内存泄漏

什么是内存泄漏?

不再需要的内存没有被释放,导致内存持续增长。

场景1:被遗忘的定时器

javascript
// ❌ 内存泄漏
function startPolling() {
  const data = fetchHugeData();

  setInterval(() => {
    // 闭包引用了 data
    process(data);
  }, 1000);
  // 如果不 clearInterval,data 永远无法回收
}

// ✅ 正确做法
function startPolling() {
  const data = fetchHugeData();

  const timer = setInterval(() => {
    process(data);
  }, 1000);

  // 提供清理方法
  return function stop() {
    clearInterval(timer);
    // timer 被清除后,回调函数被释放,data 可以被回收
  };
}

const stopPolling = startPolling();
// 不需要时调用 stopPolling()

场景2:事件监听器未移除

javascript
// ❌ 内存泄漏
function setupHandler() {
  const element = document.getElementById('button');
  const data = loadHugeData();

  element.addEventListener('click', function handler() {
    console.log(data); // 闭包引用 data
  });

  // 元素被移除,但事件监听器和闭包仍然存在
}

// ✅ 正确做法
function setupHandler() {
  const element = document.getElementById('button');
  const data = loadHugeData();

  function handler() {
    console.log(data);
  }

  element.addEventListener('click', handler);

  // 返回清理函数
  return function cleanup() {
    element.removeEventListener('click', handler);
  };
}

// 或使用 AbortController
function setupHandler() {
  const controller = new AbortController();
  const data = loadHugeData();

  element.addEventListener('click', () => {
    console.log(data);
  }, { signal: controller.signal });

  return () => controller.abort(); // 一键移除所有监听器
}

场景3:闭包循环引用

javascript
// ❌ 旧版IE中的经典泄漏(现代浏览器已解决)
function leak() {
  const element = document.getElementById('box');

  element.onclick = function() {
    // 闭包引用了 element
    console.log(element.id);
  };

  // element → onclick → 闭包 → element(循环引用)
}

// ✅ 解决方案
function noLeak() {
  const element = document.getElementById('box');
  const id = element.id; // 提取需要的值

  element.onclick = function() {
    console.log(id); // 不再引用 element
  };
}

场景4:意外的全局变量

javascript
// ❌ 意外创建全局变量
function process() {
  // 忘记 var/let/const → 自动创建全局变量
  tempData = new Array(1000000).fill('*');
}
// tempData 永远不会被回收

// ✅ 使用严格模式
'use strict';
function process() {
  tempData = []; // ❌ ReferenceError: tempData is not defined
}

场景5:console.log 保持引用

javascript
// ❌ 开发时的陷阱
function createData() {
  const data = new Array(1000000).fill('*');
  console.log(data); // DevTools 保持对 data 的引用!
  return data.length;
}

// 生产环境应移除 console.log
// 或使用构建工具(如 terser)自动移除

场景6:DOM引用未清除

javascript
// ❌ 保存DOM引用
const cache = {};

function addToCache(id) {
  cache[id] = document.getElementById(id);
}

addToCache('header');
// 即使 #header 从DOM中移除,cache.header 仍然引用它
// → Detached DOM tree(分离的DOM树)

// ✅ 使用 WeakRef
const cache2 = {};

function addToCache2(id) {
  const el = document.getElementById(id);
  cache2[id] = new WeakRef(el);
}

function getFromCache(id) {
  const ref = cache2[id];
  if (!ref) return null;
  const el = ref.deref();
  if (!el) {
    delete cache2[id]; // 已被GC
    return null;
  }
  return el;
}

Chrome DevTools 内存分析

检测内存泄漏的步骤

1. Performance Monitor(性能监视器)
   打开方式:DevTools → Ctrl+Shift+P → "Performance Monitor"
   观察:JS Heap Size 是否持续增长

2. Memory → Heap Snapshot
   - 拍摄快照1
   - 执行可能泄漏的操作
   - 拍摄快照2
   - 对比两个快照(Comparison视图)
   - 查看 Delta 列中新增的对象

3. Memory → Allocation instrumentation on timeline
   - 实时观察内存分配
   - 蓝色条 = 分配但未回收的内存
   - 灰色条 = 已回收的内存

4. 关键指标
   - Shallow Size: 对象自身占用的内存
   - Retained Size: 对象及其引用的所有对象占用的总内存
   - Detached DOM tree: 已从页面移除但仍在内存中的DOM节点

手动触发GC

javascript
// Chrome DevTools 中:
// Memory 面板 → 点击垃圾桶图标

// 或在 Timeline/Performance 面板录制时
// 点击 "Collect garbage" 按钮

// Node.js 中:
// node --expose-gc script.js
// global.gc(); // 手动触发GC

闭包的性能考量

javascript
// 1. 闭包创建有开销(但现代引擎已高度优化)
// 避免在热路径中不必要地创建闭包

// ❌ 不必要的闭包
function processArray(arr) {
  return arr.map(function(item) { // 每次 processArray 调用都创建新函数
    return item * 2;
  });
}

// ✅ 复用函数
const double = item => item * 2;
function processArray(arr) {
  return arr.map(double); // 复用同一个函数
}

// 2. 闭包变量的访问比局部变量慢(作用域链查找)
// 但差异极小,通常不需要担心

// 3. 大量闭包共享大对象时注意内存
function createHandlers(elements) {
  const sharedData = loadHugeData(); // 所有闭包共享

  // ❌ 所有 handler 都持有 sharedData 的引用
  return elements.map(el => () => process(el, sharedData));

  // ✅ 提取每个元素需要的数据
  return elements.map(el => {
    const needed = extractNeeded(sharedData, el);
    return () => process(el, needed);
  });
}

总结

闭包核心知识点:
┌──────────────────────────────────────────────────────────┐
│ 定义                                                      │
│ • 闭包 = 函数 + 定义时的词法环境                             │
│ • 内部函数引用了外部函数的变量 → 形成闭包                      │
│ • 外部函数执行完毕后,被引用的变量不会被销毁                    │
├──────────────────────────────────────────────────────────┤
│ 形成条件                                                   │
│ • JavaScript 的词法作用域(定义时确定,非调用时)               │
│ • 作用域链:内部 → 外部 → ... → 全局                        │
│ • 函数是一等公民(可以作为值传递)                             │
├──────────────────────────────────────────────────────────┤
│ 应用场景                                                   │
│ • 数据封装(模拟私有变量)                                   │
│ • 函数工厂 / 柯里化                                        │
│ • 缓存(记忆化)                                           │
│ • 防抖 / 节流                                             │
│ • 模块模式 / 单例模式                                      │
├──────────────────────────────────────────────────────────┤
│ 内存泄漏防范                                               │
│ • 及时 clearTimeout/clearInterval                         │
│ • removeEventListener 或用 AbortController                │
│ • 避免不必要的大对象引用                                     │
│ • 生产环境移除 console.log                                 │
│ • 用 WeakRef/WeakMap 做缓存                               │
│ • Chrome DevTools Memory 面板排查                          │
└──────────────────────────────────────────────────────────┘