闭包

115 阅读4分钟

闭包产生的原因

闭包是一种特殊的函数对象,它由一个函数和该函数能够访问的外部变量组成。闭包产生的原因是函数可以作为返回值,同时函数内部可以定义其他函数,这些函数可以访问外部函数的局部变量。

当内部函数被返回时,其所在的外部函数的执行环境并没有被销毁,而是被保存在返回的函数中,这样就形成了闭包。由于闭包可以访问外部函数的局部变量,所以外部函数的局部变量不会被销毁,而是被保存在闭包中,可以被内部函数随时访问。

闭包的简单例子

  1. 实现私有变量和方法(类似Java的私有属性)
function outerFunction() {
function createCounter() {
  var count = 0;

  function counter() {
    count++;
    console.log(count);
  }

  return counter;
}

var counter1 = createCounter();
counter1(); // 输出 1
counter1(); // 输出 2

var counter2 = createCounter();
counter2(); // 输出 1


在这个例子中,createCounter 函数在第一次被调用时,创建了一个闭包,将内部函数 counter 和其所在的环境一起返回。因为 counter 函数引用了 createCounter 中的 count 变量,所以 count 变量并不会随着 createCounter 函数的执行结束而销毁。因此,每次调用 counter 函数时,count 变量都可以被正确地自增。

在代码中可以看到,我们创建了两个不同的计数器 counter1counter2,每个计数器都具有自己的内部状态(即 count 变量)。这是因为每次调用 createCounter 函数时都会创建一个新的闭包,每个闭包都有自己的环境,包括独立的 count 变量。

这个例子展示了闭包的一个常见用途,即将内部函数和其所在的环境一起返回,从而创建一个“私有”环境,使得外部无法直接访问和修改内部状态。
2. 延迟执行

function delay() {
  for (var i = 1; i <= 5; i++) {
    (function(j) {
      setTimeout(function() {
        console.log(j);
      }, j * 1000);
    })(i);
  }
}

delay(); // 依次输出 1、2、3、4、5,每个数字间隔 1 秒

  1. 实现回调函数
function loadImage(src, callback) {
  var img = new Image();
  img.onload = function() {
    callback(img);
  };
  img.src = src;
}

loadImage("example.jpg", function(img) {
  console.log(img.width, img.height);
});

loadImage 函数用于加载图片,并在图片加载完成后执行回调函数。回调函数在闭包中被定义,并且可以访问 img 变量,因此可以在回调函数中操作 img 对象。

缺点

  • 内存泄漏

在 JavaScript 中,垃圾回收器会自动处理不再需要的对象和变量,以便释放内存资源。但是,由于闭包中的函数可以访问其创建时的作用域链中的变量,这些变量可能会一直被持有,而不被垃圾回收器回收,从而导致内存泄漏。

function createButton() {
  var button = document.createElement('button');
  button.innerText = 'Click me!';

  var count = 0;

  button.addEventListener('click', function() {
    count++;
    console.log('Clicked ' + count + ' times');
  });

  return button;
}

var button = createButton();
document.body.appendChild(button);

这个代码存在一个潜在的内存泄漏问题。由于闭包中的 click 事件监听器函数引用了 count 变量,而 count 变量是在 createButton 函数中创建的,它的生命周期比 createButton 函数更长。因此,每次点击按钮时,该闭包都会持有对 count 变量的引用,从而使得 count 变量无法被垃圾回收器回收,导致内存泄漏。

function createButton() {
  var button = document.createElement('button');
  button.innerText = 'Click me!';

  return (function() {
    var count = 0;

    button.addEventListener('click', function() {
      count++;
      console.log('Clicked ' + count + ' times');
    });

    return {
      destroy: function() {
        button.removeEventListener('click', clickHandler);
        clickHandler = null;
      }
    };
  })();
}

var button = createButton();
document.body.appendChild(button);

// 释放资源
button.destroy();
button = null;

解决方式:手动将 clickHandler 变量赋值为 null,以便垃圾回收器及时回收这些资源。在使用完闭包之后,我们也手动将 button 变量赋值为 null,以便释放内存资源。

  • 性能问题
    由于闭包会涉及到父函数作用域的访问和变量的拷贝等操作,因此在一些场景下会影响性能。例如:
function doSomething() {
  var result = "";

  for (var i = 0; i < 10000; i++) {
    (function() {
      result += i;
    })();
  }

  return result;
}

console.log(doSomething());


在循环内部使用了一个立即执行函数来创建闭包,从而实现对变量 result 和 i 的访问。由于该函数使用了闭包,因此执行速度比较慢。 为了避免性能问题,我们可以尽量避免在循环内部使用闭包,或者使用其他技巧来提高性能,例如将循环改为递归。

  • 作用域过长
function outer() {
  var x = 1;

  function middle() {
    var y = 2;

    function inner() {
      var z = 3;

      return x + y + z;
    }

    return inner();
  }

  return middle();
}

console.log(outer());

定义了一个 outer 函数和两个嵌套的函数 middle 和 inner。在 inner 函数中,我们通过闭包访问了 x 和 y 变量,从而实现了对它们的累加操作。由于闭包的嵌套层次比较深,因此作用域链比较长,可能会影响代码的可读性和性能。 为了避免作用域链过长的问题,我们可以尽量减少闭包的嵌套层次,或者使用其他技巧来简化代码,例如将多个函数合并为一个函数,使用类或对象等。