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.

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。