JavaScript 中的闭包的形成及使用场景

😉 你好呀,我是爱编程的Sherry,很高兴在这里遇见你!我是一名拥有十多年开发经验的前端工程师。这一路走来,面对困难时也曾感到迷茫,凭借不懈的努力和坚持,重新找到了前进的方向。我的人生格言是——认准方向,坚持不懈,你终将迎来辉煌!欢迎关注我😁,我将在这里分享工作中积累的经验和心得,希望我的分享能够给你带来帮助。

引言

闭包(Closure) 是 JavaScript 中一个非常重要且独特的概念,它指的是 函数能够记住并访问其词法作用域内的变量,即使这个函数在其词法作用域之外执行。

通俗地说,闭包是 一个函数可以“记住”它在定义时的环境(包括变量和参数),即使这个函数在它定义时的作用域之外被调用,它仍然可以访问该作用域中的变量。

闭包的形成

当一个函数内部定义并返回了另一个函数时,并且这个内部函数引用了外部函数的变量,这就形成了闭包。闭包使得内部函数能够“记住”它定义时外部函数的变量,即使外部函数已经执行结束了,这些变量仍然可以被访问。

闭包的例子
function outer() {
  let counter = 0;
  return function inner() {
    counter++;
    console.log(counter);
  }
}

const increment = outer();
increment(); // 输出: 1
increment(); // 输出: 2
increment(); // 输出: 3

在这个例子中:

outer 函数返回了 inner 函数,而 inner 函数引用了 outer 函数中的局部变量 counter。即使 outer 函数已经执行完毕,inner 函数仍然能够访问和修改 counter,因为 inner 函数形成了一个闭包,保存了对 outer 函数作用域中变量的引用。

闭包的特性

1. 函数可以记住它的词法作用域:

即使外部函数已经返回,闭包中的函数仍然可以访问外部函数作用域中的变量。

2. 局部变量的持久化:

闭包中的局部变量不会在外部函数返回后被销毁,它们会被保留,直到没有任何闭包再引用它们为止。

3. 数据封装:

闭包可以模拟私有变量,避免外部直接访问或修改这些变量,只能通过闭包函数来操作它们。

闭包的使用场景

闭包的应用场景非常广泛,以下是一些常见的使用场景:

1. 模拟私有变量

JavaScript 中没有真正的私有变量,但闭包可以帮助实现类似的效果。通过闭包,可以创建只能通过特定函数访问和修改的变量,避免外部直接访问。

function createCounter() {
  let count = 0;
  return {
    increment: function() {
      count++;
      console.log(count);
    },
    decrement: function() {
      count--;
      console.log(count);
    }
  }
}

const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
counter.decrement(); // 输出: 1

在这个例子中,countcreateCounter 函数中的局部变量,外部无法直接访问它,只能通过 incrementdecrement 方法修改它的值。这种方式模拟了私有变量的行为。

2. 函数工厂

闭包可以用于创建返回不同功能的函数,类似于工厂模式。例如,可以创建多个配置不同的计数器:

function createCounter(start) {
  let count = start;
  return function() {
    count++;
    console.log(count);
  }
}

const counter1 = createCounter(0);
const counter2 = createCounter(100);

counter1(); // 输出: 1
counter1(); // 输出: 2
counter2(); // 输出: 101
counter2(); // 输出: 102

每次调用 createCounter 都会生成一个新的计数器实例,且每个计数器都能记住自己的初始值。

3. 回调函数和事件处理

闭包广泛应用于回调函数、事件处理等异步编程场景中。例如,在事件处理函数中,闭包可以保存外部函数的状态,并在事件触发时访问这些状态。

function setupEventListeners() {
  let name = 'Button clicked!';
  document.getElementById('myButton').addEventListener('click', function() {
    console.log(name); // 即使外部函数已执行完毕,仍然能访问 name
  });
}

setupEventListeners();

即使 setupEventListeners 函数执行完毕后,点击按钮时仍然可以访问 name 变量,这是因为 addEventListener 的回调函数形成了闭包。

4. 防抖与节流

闭包常用于防抖和节流函数的实现,以保存计时器状态或限制函数执行频率。例如,防抖函数在用户停止输入一段时间后才执行,可以避免多次频繁的操作。

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  }
}

const handleInput = debounce(function(event) {
  console.log('Input event triggered', event.target.value);
}, 500);

document.getElementById('searchInput').addEventListener('input', handleInput);

在这个例子中,闭包用于保存 timer 变量的状态,使得每次用户输入时,timer 不会被销毁,从而实现防抖效果。

5. 缓存(记忆化)

闭包还可以用于缓存计算结果,提高函数的效率。例如,记忆化函数可以缓存已经计算过的结果,避免重复计算。

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    }
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  }
}

function slowFunction(num) {
  console.log('Expensive calculation...');
  return num * 2;
}

const memoizedFunction = memoize(slowFunction);
console.log(memoizedFunction(5)); // 输出: Expensive calculation... 10
console.log(memoizedFunction(5)); // 输出: 10 (缓存结果,没有再计算)

在这里,闭包用于保存计算结果的 cache,当相同的参数再次传递时,直接返回缓存的结果。

闭包的优缺点

优点:
1. 数据封装:

闭包可以帮助创建私有变量,使得某些数据只能通过特定的函数进行访问和修改。

2. 保持状态:

闭包可以保持函数执行时的环境,使得外部函数返回后,依然能够访问和操作这些状态。

3. 减少全局变量的使用:

闭包可以避免使用全局变量,减少变量污染和命名冲突。

缺点:
1. 内存占用:

由于闭包持有外部函数作用域的引用,可能会导致内存得不到及时释放,增加内存消耗,甚至可能引发内存泄漏。

2. 调试难度:

闭包可能导致调试更加复杂,因为内部函数依赖外部的上下文环境,容易引发不可预料的行为。

闭包的回收

function outer() {
    let count = 0;

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

    return inner;
}

const counter = outer();
counter(); // 1
counter(); // 2

在这个例子中:

  • inner 函数形成了一个闭包,引用了 outer 中的变量 count
  • 即使 outer() 执行完毕,它也不会被垃圾回收,因为 counter(即 inner)仍然在引用 count

一、什么情况下 outer 中的变量会被回收?
当闭包不再被引用时

只要闭包函数 inner 还存在引用(比如赋值给了 counter),它所捕获的外部变量(如 count)就不会被垃圾回收。

解决方法:手动解除引用。

counter = null; // 断开对 inner 的引用

此时如果没有任何其他引用指向 inner 或其捕获的变量,JavaScript 的垃圾回收器(GC)会在适当的时候回收 outer 中的变量(包括 count)。


二、闭包导致内存无法释放的原因

闭包会保持对其词法作用域内所有变量的引用,即使这些变量中有些你并没有使用。

例如:

function outer() {
    const bigData = new Array(1000000).fill('*');

    function inner() {
        console.log('Hello');
    }

    return inner;
}

虽然 inner 没有用到 bigData,但根据 JavaScript 引擎的实现方式(尤其是 V8),如果 bigDatainner 在同一个作用域中定义,V8 通常还是会保留整个作用域中的变量 —— 导致 bigData 也无法被回收。

📌 这就是所谓的“意外闭包”造成的内存问题。


三、如何优化或避免不必要的闭包占用内存?
方法一:显式断开引用
let counter = outer();

counter(); // 使用闭包

counter = null; // 显式断开引用,允许 outer 变量被回收
方法二:避免在闭包中创建大对象

不要在闭包外层函数中声明大型数据结构,除非确实需要它们被闭包访问。

❌ 不推荐:

function outer() {
    const hugeArray = createHugeArray(); // 大型数组
    return function inner() {
        console.log('Useless access to huge array?');
    };
}

✅ 推荐:

function outer() {
    return function inner() {
        const smallData = 'Just a small string';
        console.log(smallData);
    };
}

四、使用模块模式或 WeakMap 等方式管理状态

如果你希望更精细地控制生命周期,可以用设计模式来封装状态,而不是完全依赖闭包。

例如使用 WeakMap 存储私有状态:

const _data = new WeakMap();

class MyClass {
    constructor(data) {
        _data.set(this, data);
    }

    logData() {
        console.log(_data.get(this));
    }
}

这样当类实例被销毁后,_data 中对应的数据也会被回收。


五、使用 DevTools 检查内存泄漏

你可以通过 Chrome DevTools 的 Memory 面板

  • 查看闭包是否导致内存持续增长;
  • 分析对象保留树(retaining tree);
  • 判断哪些闭包没有被及时释放。

六、让 outer 被回收的关键点
条件是否能回收
inner 没有被返回或引用✅ 会被回收
inner 被返回并赋值给变量(如 counter❌ 不会被回收
counter = null,断开引用✅ 会被回收
闭包内部引用了大量无用数据❌ 可能造成内存浪费
使用 WeakMap、模块化等替代方案✅ 更好地管理内存

总结

闭包是 JavaScript 中的一个强大特性,它允许函数在词法作用域之外保持对外部变量的引用。它常用于数据封装回调函数工厂函数防抖/节流等场景。虽然闭包非常强大,但在使用时也要注意内存管理,避免不必要的内存占用和泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sherry Tian

打赏1元鼓励作者

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值