闭包是什么?优缺点、怎么防内存泄漏?

闭包是什么?优缺点、怎么防内存泄漏?

📖 导读:闭包是 JavaScript 中最重要也最容易混淆的概念之一。它既是面试必考题,也是日常开发中的利器。本文将从基础到实战,带你彻底理解闭包,并掌握如何避免其带来的内存泄漏问题。


🎯 什么是闭包?

官方定义

闭包(Closure) 是指有权访问另一个函数作用域中变量的函数。

简单来说:当一个函数能够记住并访问它的词法作用域时,就产生了闭包。

通俗理解

想象一个场景:

  • 你有一个盒子(外层函数)
  • 盒子里有一些玩具(变量)
  • 你把一个小机器人(内层函数)放进盒子
  • 即使把盒子盖上拿走,小机器人仍然可以玩盒子里的玩具

这个小机器人就是闭包


💡 基础示例

最简单的闭包

javascript 复制代码
function outer() {
  const name = '张三'; // 外层函数的变量

  function inner() {
    console.log(name); // 内层函数访问外层变量
  }

  return inner; // 返回内层函数
}

const closure = outer();
closure(); // "张三" - 即使 outer 已经执行完毕,仍能访问 name

关键点

  1. inner 函数在 outer 外部被调用
  2. inner 仍然可以访问 outer 中的 name 变量
  3. ✅ 这就是闭包!

🔍 闭包的原理

作用域链

JavaScript 采用词法作用域(静态作用域),函数的作用域在定义时就确定了。

javascript 复制代码
function outer() {
  const a = 10;

  function middle() {
    const b = 20;

    function inner() {
      const c = 30;
      console.log(a + b + c); // 60
    }

    return inner;
  }

  return middle;
}

const fn = outer()();
fn(); // 60

作用域链查找过程

复制代码
inner 的作用域链:
1. inner 自身作用域 (c)
2. middle 的作用域 (b)
3. outer 的作用域 (a)
4. 全局作用域

内存中的表现

javascript 复制代码
function createCounter() {
  let count = 0;

  return function () {
    count++;
    return count;
  };
}

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

内存结构

复制代码
createCounter 执行完毕后:
- 正常情况下,count 应该被销毁
- 但因为闭包的存在,count 仍然保存在内存中
- counter 函数持有对 count 的引用

✨ 闭包的优点

1. 数据封装与私有变量

闭包可以创建私有变量,外部无法直接访问。

javascript 复制代码
function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有变量

  return {
    deposit(amount) {
      if (amount > 0) {
        balance += amount;
        console.log(`存入 ${amount},余额:${balance}`);
      }
    },

    withdraw(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        console.log(`取出 ${amount},余额:${balance}`);
      } else {
        console.log("余额不足");
      }
    },

    getBalance() {
      return balance;
    },
  };
}

const account = createBankAccount(1000);
account.deposit(500); // 存入 500,余额:1500
account.withdraw(200); // 取出 200,余额:1300
console.log(account.getBalance()); // 1300

// ❌ 无法直接修改 balance
// account.balance = 999999; // 无效

优势

  • ✅ 保护数据不被外部随意修改
  • ✅ 提供受控的访问接口

2. 函数柯里化(Currying)

将多参数函数转换为一系列单参数函数。

javascript 复制代码
// 普通函数
function add(a, b, c) {
  return a + b + c;
}

// 柯里化版本
function curriedAdd(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    };
  };
}

// 使用
console.log(curriedAdd(1)(2)(3)); // 6

// 可以部分应用
const addFive = curriedAdd(5);
const addFiveAndTen = addFive(10);
console.log(addFiveAndTen(15)); // 30

实际应用场景

javascript 复制代码
// 创建特定类型的验证器
function createValidator(type) {
  return function (value) {
    switch (type) {
      case "email":
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
      case "phone":
        return /^1[3-9]\d{9}$/.test(value);
      case "idCard":
        return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(value);
      default:
        return false;
    }
  };
}

const validateEmail = createValidator("email");
const validatePhone = createValidator("phone");

console.log(validateEmail("test@example.com")); // true
console.log(validatePhone("13800138000")); // true

3. 防抖(Debounce)与节流(Throttle)

防抖函数
javascript 复制代码
function debounce(func, delay) {
  let timer = null;

  return function (...args) {
    // 清除上一次的定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 重新设置定时器
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// 使用场景:搜索框输入
const searchInput = document.getElementById("search");
searchInput.addEventListener(
  "input",
  debounce(function (e) {
    console.log("搜索:", e.target.value);
    // 发起搜索请求
  }, 300),
);
节流函数
javascript 复制代码
function throttle(func, interval) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastTime >= interval) {
      lastTime = now;
      func.apply(this, args);
    }
  };
}

// 使用场景:滚动事件
window.addEventListener(
  "scroll",
  throttle(function () {
    console.log("滚动位置:", window.scrollY);
  }, 200),
);

4. 模块化模式

javascript 复制代码
const CounterModule = (function () {
  let count = 0; // 私有变量

  function increment() {
    count++;
    console.log(`当前计数:${count}`);
  }

  function decrement() {
    count--;
    console.log(`当前计数:${count}`);
  }

  function getCount() {
    return count;
  }

  // 只暴露公共接口
  return {
    increment,
    decrement,
    getCount,
  };
})();

// 使用
CounterModule.increment(); // 当前计数:1
CounterModule.increment(); // 当前计数:2
CounterModule.decrement(); // 当前计数:1
console.log(CounterModule.getCount()); // 1

// ❌ 无法直接访问 count
// console.log(CounterModule.count); // undefined

5. 迭代器和生成器

javascript 复制代码
function createIterator(array) {
  let index = 0;

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

const iterator = createIterator([1, 2, 3, 4, 5]);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }

⚠️ 闭包的缺点

1. 内存占用增加

闭包会保持对外部变量的引用,导致这些变量无法被垃圾回收。

javascript 复制代码
function createBigClosure() {
  const largeArray = new Array(1000000).fill("data"); // 大数组

  return function () {
    console.log(largeArray.length); // 引用了 largeArray
  };
}

const closure = createBigClosure();
// largeArray 无法被回收,即使不再需要

2. 性能问题

频繁创建闭包会影响性能。

javascript 复制代码
// ❌ 不好的做法 - 每次循环都创建新闭包
for (let i = 0; i < 10000; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}

// ✅ 好的做法 - 使用 let 块级作用域
for (let i = 0; i < 10000; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}

3. 调试困难

闭包可能导致变量作用域复杂,难以追踪。

javascript 复制代码
function outer() {
  let x = 10;

  function middle() {
    let y = 20;

    function inner() {
      let z = 30;
      console.log(x + y + z); // 需要追踪三个作用域
    }

    return inner;
  }

  return middle;
}

4. 意外的副作用

javascript 复制代码
// 经典面试题
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // 输出 5 个 5
  }, 100);
}

// 原因:var 没有块级作用域,所有闭包共享同一个 i

🛡️ 如何防止内存泄漏?

什么是内存泄漏?

当闭包引用的变量不再需要,但仍然存在于内存中,无法被垃圾回收时,就发生了内存泄漏

常见场景及解决方案

场景一:全局变量引用
javascript 复制代码
// ❌ 不好的做法
let globalData = null;

function processData() {
  const largeData = new Array(1000000).fill("data");

  globalData = function () {
    console.log(largeData.length);
  };
}

processData();
// largeData 永远不会被回收

// ✅ 好的做法
function processData() {
  const largeData = new Array(1000000).fill("data");

  const result = largeData.length;

  // 使用完后立即释放
  return function () {
    console.log(result); // 只保留需要的数据
  };
}

const handler = processData();
handler();
handler = null; // 手动解除引用
场景二:DOM 事件监听器
javascript 复制代码
// ❌ 不好的做法 - 忘记移除事件监听器
function setupListener() {
  const element = document.getElementById("myButton");
  const data = new Array(1000000).fill("data");

  element.addEventListener("click", function () {
    console.log(data.length); // 引用了大数据
  });
}

setupListener();
// 即使元素被移除,data 也无法回收

// ✅ 好的做法 - 及时清理事件监听器
function setupListener() {
  const element = document.getElementById("myButton");
  const data = new Array(1000000).fill("data");

  function handleClick() {
    console.log(data.length);
  }

  element.addEventListener("click", handleClick);

  // 提供清理函数
  return function cleanup() {
    element.removeEventListener("click", handleClick);
  };
}

const cleanup = setupListener();
// 当不需要时调用
cleanup();
场景三:定时器未清除
javascript 复制代码
// ❌ 不好的做法
function startTimer() {
  const data = {
    /* 大量数据 */
  };

  setInterval(function () {
    console.log(data);
  }, 1000);
}

startTimer();
// 定时器永远运行,data 无法回收

// ✅ 好的做法
function startTimer() {
  const data = {
    /* 大量数据 */
  };

  const timerId = setInterval(function () {
    console.log(data);
  }, 1000);

  // 返回清理函数
  return function stopTimer() {
    clearInterval(timerId);
  };
}

const stopTimer = startTimer();
// 当不需要时停止
setTimeout(stopTimer, 5000);
场景四:Vue/React 组件中的闭包
javascript 复制代码
// Vue 示例
export default {
  mounted() {
    const largeData = new Array(1000000).fill("data");

    // ❌ 忘记在 beforeDestroy 中清理
    this.timer = setInterval(() => {
      console.log(largeData);
    }, 1000);
  },

  // ✅ 正确的做法
  beforeUnmount() {
    // Vue 3
    // beforeDestroy() { // Vue 2
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  },
};

// React 示例
function MyComponent() {
  useEffect(() => {
    const largeData = new Array(1000000).fill("data");

    const timer = setInterval(() => {
      console.log(largeData);
    }, 1000);

    // ✅ 返回清理函数
    return () => {
      clearInterval(timer);
    };
  }, []);

  return <div>My Component</div>;
}
场景五:缓存管理
javascript 复制代码
// ❌ 不好的做法 - 无限增长的缓存
const cache = {};

function getData(id) {
  if (cache[id]) {
    return cache[id];
  }

  const data = fetchLargeData(id); // 获取大数据
  cache[id] = data; // 永久保存
  return data;
}

// ✅ 好的做法 - 使用 WeakMap 或限制缓存大小
class CacheManager {
  constructor(maxSize = 100) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }

  get(key) {
    return this.cache.get(key);
  }

  set(key, value) {
    // 如果超过最大容量,删除最旧的条目
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }

    this.cache.set(key, value);
  }

  clear() {
    this.cache.clear();
  }
}

const cache = new CacheManager(100);

通用防内存泄漏策略

1. 及时解除引用
javascript 复制代码
let data = loadData(); // 加载大数据

// 使用数据
processData(data);

// ✅ 使用完后立即释放
data = null;
2. 使用弱引用(WeakMap/WeakSet)
javascript 复制代码
// WeakMap 的键是弱引用,可以被垃圾回收
const weakCache = new WeakMap();

function associateData(obj, data) {
  weakCache.set(obj, data);
}

function getData(obj) {
  return weakCache.get(obj);
}

// 当 obj 被回收时,对应的数据也会被回收
3. 避免不必要的全局变量
javascript 复制代码
// ❌ 不好的做法
window.myAppData = {
  /* 大量数据 */
};

// ✅ 好的做法 - 使用模块或闭包封装
(function () {
  const myAppData = {
    /* 大量数据 */
  };

  // 只暴露必要的接口
  window.myApp = {
    getData() {
      return myAppData;
    },
  };
})();
4. 定期检查和清理
javascript 复制代码
// 定期检查内存使用情况
if (window.performance && window.performance.memory) {
  setInterval(() => {
    const memory = window.performance.memory;
    console.log("已用内存:", memory.usedJSHeapSize);
    console.log("总内存:", memory.totalJSHeapSize);

    // 如果内存使用过高,触发清理
    if (memory.usedJSHeapSize > memory.totalJSHeapSize * 0.8) {
      console.warn("内存使用过高,建议清理");
      // 执行清理操作
      cleanupUnusedResources();
    }
  }, 30000); // 每 30 秒检查一次
}
5. 使用 Chrome DevTools 检测内存泄漏

步骤

  1. 打开 Chrome DevTools
  2. 切换到 Memory 面板
  3. 选择 Heap snapshot
  4. 拍摄快照
  5. 执行操作
  6. 再次拍摄快照
  7. 比较两个快照,查找未释放的对象

💼 项目中的实际应用

应用一:API 请求封装

javascript 复制代码
// utils/request.js
function createRequest(baseURL) {
  const defaultHeaders = {
    "Content-Type": "application/json",
  };

  // 拦截器
  const interceptors = {
    request: [],
    response: [],
  };

  async function request(url, options = {}) {
    const fullUrl = `${baseURL}${url}`;

    // 执行请求拦截器
    let config = {
      ...options,
      headers: { ...defaultHeaders, ...options.headers },
    };

    for (const interceptor of interceptors.request) {
      config = await interceptor(config);
    }

    try {
      const response = await fetch(fullUrl, config);
      let data = await response.json();

      // 执行响应拦截器
      for (const interceptor of interceptors.response) {
        data = await interceptor(data);
      }

      return data;
    } catch (error) {
      console.error("请求失败:", error);
      throw error;
    }
  }

  // 添加拦截器
  request.addRequestInterceptor = function (interceptor) {
    interceptors.request.push(interceptor);
  };

  request.addResponseInterceptor = function (interceptor) {
    interceptors.response.push(interceptor);
  };

  return request;
}

// 使用
const api = createRequest("https://api.example.com");

// 添加 token 拦截器
api.addRequestInterceptor((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 添加错误处理拦截器
api.addResponseInterceptor((data) => {
  if (data.code !== 200) {
    throw new Error(data.message);
  }
  return data.data;
});

// 发起请求
api("/users").then((users) => {
  console.log(users);
});

应用二:状态管理(简化版 Redux)

javascript 复制代码
// store/index.js
function createStore(reducer, initialState) {
  let state = initialState;
  const listeners = [];

  function getState() {
    return state;
  }

  function dispatch(action) {
    state = reducer(state, action);

    // 通知所有订阅者
    listeners.forEach((listener) => listener());
  }

  function subscribe(listener) {
    listeners.push(listener);

    // 返回取消订阅函数
    return function unsubscribe() {
      const index = listeners.indexOf(listener);
      if (index > -1) {
        listeners.splice(index, 1);
      }
    };
  }

  return {
    getState,
    dispatch,
    subscribe,
  };
}

// 使用
const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const store = createStore(reducer);

// 订阅状态变化
const unsubscribe = store.subscribe(() => {
  console.log("状态变化:", store.getState());
});

store.dispatch({ type: "INCREMENT" }); // 状态变化: { count: 1 }
store.dispatch({ type: "INCREMENT" }); // 状态变化: { count: 2 }

// 取消订阅
unsubscribe();

应用三:路由守卫

javascript 复制代码
// router/guard.js
function createRouterGuard(router) {
  const guards = {
    beforeEach: [],
    afterEach: [],
  };

  // 注册前置守卫
  function beforeEach(guard) {
    guards.beforeEach.push(guard);
  }

  // 注册后置守卫
  function afterEach(guard) {
    guards.afterEach.push(guard);
  }

  // 执行路由跳转
  async function navigate(to, from) {
    // 执行前置守卫
    for (const guard of guards.beforeEach) {
      const result = await guard(to, from);
      if (result === false) {
        return false; // 阻止跳转
      }
    }

    // 执行路由跳转
    router.push(to);

    // 执行后置守卫
    guards.afterEach.forEach((guard) => {
      guard(to, from);
    });

    return true;
  }

  return {
    beforeEach,
    afterEach,
    navigate,
  };
}

// 使用
const guard = createRouterGuard(router);

// 登录验证
guard.beforeEach((to, from) => {
  const token = localStorage.getItem("token");

  if (to.meta.requiresAuth && !token) {
    // 未登录,跳转到登录页
    router.push("/login");
    return false;
  }

  return true;
});

// 记录页面访问
guard.afterEach((to, from) => {
  console.log(`从 ${from.path} 跳转到 ${to.path}`);
});

🔧 常见面试题解析

Q1: 下面代码输出什么?

javascript 复制代码
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}

A : 输出 5 个 5

原因var 没有块级作用域,所有闭包共享同一个 i。当定时器执行时,循环已经结束,i 的值为 5。

解决方案

javascript 复制代码
// 方案一:使用 let
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // 0, 1, 2, 3, 4
  }, 100);
}

// 方案二:使用 IIFE
for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(function () {
      console.log(j); // 0, 1, 2, 3, 4
    }, 100);
  })(i);
}

Q2: 如何实现一个 once 函数,只执行一次?

javascript 复制代码
function once(fn) {
  let executed = false;
  let result;

  return function (...args) {
    if (!executed) {
      executed = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

// 使用
const initialize = once(function () {
  console.log("初始化...");
  return "initialized";
});

initialize(); // "初始化..."
initialize(); // 无输出
initialize(); // 无输出

Q3: 闭包会导致内存泄漏吗?

A : 不一定。闭包本身不会导致内存泄漏,只有当闭包引用的变量不再需要但无法被回收时,才会造成内存泄漏。

关键

  • ✅ 合理使用闭包是正常的
  • ❌ 忘记清理不需要的引用才会导致泄漏

Q4: 如何判断是否存在内存泄漏?

A:

  1. 使用 Chrome DevTools 的 Memory 面板
  2. 观察内存使用是否持续增长
  3. 进行堆快照对比
  4. 检查是否有未清理的定时器、事件监听器等

📊 闭包 vs 其他方案对比

特性 闭包 Class Module
数据私有性 ⚠️(需约定)
内存占用 ⚠️(需注意)
代码可读性 ⚠️
适用场景 小型工具函数 复杂对象 大型模块

🎓 进阶知识

闭包的性能优化

javascript 复制代码
// ❌ 不好的做法 - 每次都创建新函数
function createGreeter(name) {
  return function () {
    console.log(`Hello, ${name}!`);
  };
}

// ✅ 好的做法 - 缓存函数
const greeterCache = new Map();

function getGreeter(name) {
  if (!greeterCache.has(name)) {
    greeterCache.set(name, function () {
      console.log(`Hello, ${name}!`);
    });
  }
  return greeterCache.get(name);
}

尾调用优化

javascript 复制代码
// ❌ 可能导致栈溢出
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

// ✅ 尾递归优化(需要引擎支持)
function factorialTail(n, accumulator = 1) {
  if (n <= 1) return accumulator;
  return factorialTail(n - 1, n * accumulator);
}

📚 参考资源


🙋 常见问题

Q1: 箭头函数有闭包吗?

A: 有!箭头函数同样可以形成闭包。

javascript 复制代码
function outer() {
  const name = "张三";

  return () => {
    console.log(name); // 形成闭包
  };
}

Q2: 闭包和回调函数有什么区别?

A:

  • 闭包:是一种机制,函数可以访问其词法作用域
  • 回调函数:是一种编程模式,作为参数传递的函数
  • 回调函数可以是闭包,但不一定是

Q3: 如何调试闭包?

A:

  1. 使用 Chrome DevTools 的 Sources 面板
  2. 在闭包函数中设置断点
  3. 查看 Scope 面板中的作用域链
  4. 观察 Closure 下的变量

🎉 结语

闭包是 JavaScript 的强大特性,正确使用它可以:

  • ✅ 实现数据封装和私有变量
  • ✅ 创建灵活的函数工厂
  • ✅ 实现防抖、节流等实用功能
  • ✅ 构建模块化架构

但也要注意:

  • ⚠️ 避免不必要的内存占用
  • ⚠️ 及时清理不需要的引用
  • ⚠️ 注意性能影响

记住三个原则

  1. 按需使用:只在必要时使用闭包
  2. 及时清理:不再使用时解除引用
  3. 监控内存:定期检查内存使用情况

掌握闭包,让你的 JavaScript 代码更加优雅和高效!


💬 互动时间:你在项目中如何使用闭包的?遇到过哪些内存泄漏问题?欢迎在评论区分享你的经验!

如果觉得这篇文章对你有帮助,请点赞👍、收藏⭐、转发🔄,让更多人受益!

相关推荐
云水一下6 小时前
Vue.js从零到精通系列(二):响应式核心——ref、reactive、computed与watch
前端·javascript·vue.js
放下华子我只抽RuiKe56 小时前
FastAPI 全栈后端(二):路由与数据模型
前端·人工智能·react.js·前端框架·html·fastapi
lichenyang4536 小时前
ArkTS 严格类型系统:我答错 2 道题后才真正搞懂的几条规则
前端
小小小小宇6 小时前
定高、不定高、瀑布流虚拟列表
前端
天启HTTP6 小时前
开启全局代理后网络变慢,问题出在哪
开发语言·前端·网络·tcp/ip·php
卡布鲁7 小时前
Webpack 核心原理与自定义 Loader/Plugin 实战
前端·javascript
智码看视界7 小时前
Web Storage 的无障碍实践与工程化应用
前端·javascript·web
孟陬7 小时前
国外技术周刊 #140:在 Jeff Bezos 的私密 Campfire 峰会上,我学到了关于亿万富翁的事
前端·后端
槑有老呆7 小时前
Bun:一个让 Node 开发者原地起飞的 JS/TS 运行时
前端
小小小小宇7 小时前
AI Agent 核心流程与底层逻辑
前端