icon
闭包

闭包

闭包是指有权访问另一个函数作用域中变量的函数。闭包的形成需要满足两个条件:

  1. 函数嵌套
  2. 内部函数引用外部函数的变量

基本概念

function outer() {
  var outerVar = 'outer';
  
  function inner() {
    console.log(outerVar); // 访问外部函数的变量
  }
  
  return inner;
}

const closure = outer();
closure(); // 'outer'

闭包的作用

  1. 保护变量
    • 避免全局变量污染
    • 实现私有变量
function createCounter() {
  let count = 0; // 私有变量
  
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// count 无法直接访问,实现了封装
  1. 保存状态
    • 在异步操作中保存变量状态
function createTimer() {
  let startTime = Date.now();
  
  return function() {
    return Date.now() - startTime;
  };
}

const timer = createTimer();
setTimeout(() => {
  console.log(timer()); // 输出经过的时间
}, 1000);

常见应用场景

  1. 模块化
const module = (function() {
  let privateVar = 0;
  
  return {
    publicMethod: function() {
      privateVar++;
      return privateVar;
    }
  };
})();

module.publicMethod(); // 1
  1. 函数工厂
function createMultiplier(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

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

console.log(double(5)); // 10
console.log(triple(5)); // 15
  1. 事件处理
function setupButton(buttonId, message) {
  const button = document.getElementById(buttonId);
  
  button.addEventListener('click', function() {
    alert(message); // 闭包保存了 message
  });
}

setupButton('btn1', 'Hello');
setupButton('btn2', 'World');

闭包与循环

经典问题:循环中使用闭包

// 问题代码
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出 3 次 3
  }, 100);
}

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

// 解决方案2:使用 let
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 0, 1, 2
  }, 100);
}

// 解决方案3:使用 bind
for (var i = 0; i < 3; i++) {
  setTimeout(function(j) {
    console.log(j);
  }.bind(null, i), 100);
}

内存泄漏问题

闭包可能导致内存泄漏,需要注意:

// 可能导致内存泄漏
function attachHandler() {
  const element = document.getElementById('button');
  element.onclick = function() {
    // 闭包引用了 element,即使元素被移除,内存也不会释放
    element.style.backgroundColor = 'red';
  };
}

// 改进:避免不必要的引用
function attachHandler() {
  const element = document.getElementById('button');
  const id = element.id;
  element.onclick = function() {
    // 只引用需要的属性
    document.getElementById(id).style.backgroundColor = 'red';
  };
}

性能考虑

  • 闭包会占用更多内存
  • 过度使用闭包可能影响性能
  • 在不需要时及时解除引用
// 使用完毕后解除引用
let closure = createClosure();
closure(); // 使用
closure = null; // 解除引用,帮助垃圾回收