Skip to content

闭包

闭包 是指一个函数可以访问并操作其外部作用域中的变量。简单来说,当一个函数嵌套在另一个函数内部时,内部函数可以访问外部函数的变量,这就是闭包。

闭包的原理可以归结为两点:

  1. 函数可以作为参数或返回值传递;
  2. 函数可以访问其外部作用域的变量;

常见示例

模块化

通过闭包,可以创建私有变量和方法,从而实现模块化。这有助于保护内部实现细节,避免全局变量污染。

js
function createCounter() {
  let count = 0;

  function increment() {
    count++;
  }

  function getCount() {
    return count;
  }

  return { increment, getCount };
}

const { increment, getCount } = createCounter();

increment();
console.log(getCount()); // 1

事件处理

闭包可以用于在事件处理程序中保存状态信息。

js
function createButton(text) {
  let clickCount = 0;

  const button = document.createElement("button");
  button.innerText = text;
  button.addEventListener("click", () => {
    clickCount++;
    console.log(`${text} button clicked ${clickCount} times.`);
  });

  return button;
}

const button = createButton("按钮");
document.body.appendChild(button);

函数柯里化

闭包可以用于实现函数柯里化,将多参数函数转换为一系列单参数函数。

js
function curry(fn) {
  // fn.length获取当前函数的参数个数
  const arity = fn.length;

  function curried(...args) {
    // 当参数大于 arity 时,执行该函数 
    if (args.length >= arity) {
      return fn.apply(this, args);
    } else {
      // 参数少于 arity 时,会直接返回当前函数,以便链式调用
      return function (...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  }

  return curried;
}

function add(a, b, c) {
  return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6

延迟执行

闭包可以用来延迟执行函数,避免在短时间内频繁执行一个函数。

js
function debounce(fn, delay) {
  let timeoutId;

  return function () {
    const that = this;
    const args = arguments;

    // 先清除上一次的定时器
    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      fn.apply(that, args);
    }, delay);
  };
}

function doSomething() {
  console.log("Doing something...");
}

const d = debounce(doSomething, 1000);
d(); // Doing something...

缓存数据

通过闭包来缓存数据,可以避免重复计算,提高效率。

js
function fibonacci() {
  const cache = {};

  function fib(n) {
    if (n < 2) {
      return 1;
    }

    // 缓存数据,提高效率
    if (cache[n]) {
      return cache[n];
    }

    cache[n] = fib(n - 1) + fib(n - 2);
    return cache[n];
  }

  return fib;
}

const fib = fibonacci();

// 循环输出前 20 项
// 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610  987 1597 2584
for (let i = 0; i <= 20; i++) {
  console.log(fib(i));
}
js
function fibonacci(n) {
  if (n < 2) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

for (let i = 0; i < 20; i++) {
  console.log(fibonacci(i));
}

闭包缺点

虽然闭包很有用,但也需要注意一下两点:

  1. 内存泄漏:由于闭包会引用外部作用域的变量,这可能导致内存泄漏。当不再需要闭包时,应该确保解除对外部变量的引用,以便垃圾回收器回收内存;
  2. 性能:过度使用闭包可能导致性能下降,谨慎使用闭包;

如何避免闭包内存泄漏

闭包会引用外部函数中的变量和函数,如果这些变量和函数没有及时释放,就可能导致内存泄漏的问题。

手动清除

在闭包的最后,手动将外部变量和函数置为 null,以释放内存。

js
function outer() {
  let a = 1;

  return function inner() {
    console.log(1);

    a = null;
    inner = null;
  };
}

const out = outer();
out();

使用立即执行函数

使用立即执行函数创建一个独立的作用域,并在作用域结束时自动清除变量和函数。

js
function outer() {
  let a = 1;

  return (function inner() {
    console.log(1);
  })();
}

const out = outer();

避免循环引用

当闭包和外部对象之间存在循环引用时,需要特别注意变量的清除。可以在外部对象中定义一个方法来清除闭包变量。

js
function outer() {
  let a = 1;

  const obj = {
    inner: () => {
      console.log(a);
    },
    clear: () => {
      a = null;
      obj.inner = null;
    }
  };

  return obj;
}

const out = outer();

Released under the MIT License.