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

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

📖 导读:闭包是 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 代码更加优雅和高效!


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

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

相关推荐
jay神1 小时前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神1 小时前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat1 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
Thanks_ks1 小时前
HTTP 请求方法与 RESTful 接口设计:GET 和 POST 到底怎么用
http·api·restful·状态码·get·post·接口设计
miss2 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan3 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真3 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment3 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan3 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么3 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模