JavaScript 闭包详解

2 阅读3分钟

前言

在 JavaScript 中,闭包是一个非常经典、也非常高频的知识点。
很多人在刚接触闭包时,会觉得这个概念有点抽象,但实际上,只要结合代码去理解,闭包并没有那么难。

闭包在实际开发中非常常见,比如:

  • 计数器
  • 函数工厂
  • 私有变量
  • 回调函数
  • 模块化封装

本文就来系统讲清楚:什么是闭包、闭包有什么作用、实际开发中怎么用。


一、什么是闭包?

闭包可以简单理解为:

函数 + 它定义时所处的作用域环境

换句话说:

一个函数即使在外部执行,仍然可以访问它定义时所在作用域中的变量,这种现象就和闭包有关。

来看一个经典例子:

function outer() {
  let count = 0;

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

  return inner;
}

const fn = outer();

fn(); // 1
fn(); // 2
fn(); // 3

这里的

inner

就形成了闭包。

原因是:

  • outer()

    执行结束后,本来它内部变量

    count

    应该被销毁

  • 但是

    inner

    还在使用

    count

  • 所以

    count

    会继续保存在内存中


二、闭包的核心特点

1)可以访问外部函数的变量

function outer() {
  let name = 'Tom';

  function inner() {
    console.log(name);
  }

  return inner;
}

const fn = outer();
fn(); // Tom

2)可以让变量长期保存

function createCounter() {
  let count = 0;

  return function () {
    count++;
    return count;
  };
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

这里

count

不会因为

createCounter()

执行结束就消失,而是会一直保留。


三、为什么需要闭包?

闭包主要有几个作用:

1)保存状态

最典型的就是计数器。

2)实现私有变量

让某些数据不能被外部直接修改。

3)避免全局变量污染

把变量封装在函数内部。

4)在异步回调中保留上下文数据

例如定时器、事件回调中保存当前值。


四、闭包的实际应用

场景 1:计数器

function createCounter() {
  let count = 0;

  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();

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

场景 2:私有变量

function createUser(name) {
  let password = '123456';

  return {
    getName() {
      return name;
    },
    checkPassword(value) {
      return value === password;
    }
  };
}

const user = createUser('Alice');

console.log(user.getName()); // Alice
console.log(user.checkPassword('123456')); // true
console.log(user.password); // undefined

这里

password

外部无法直接访问。


五、经典面试题:循环中的闭包

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

输出结果:

3
3
3

原因是:

  • var

    没有块级作用域

  • 循环结束后

    i

    变成了

    3

  • 定时器执行时,拿到的都是同一个

    i


解决方式 1:使用let

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

输出:

0
1
2

解决方式 2:使用闭包

for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j);
    }, 1000);
  })(i);
}

六、闭包的注意点

闭包虽然强大,但也不能滥用。

注意点:

  • 闭包会让变量继续存活
  • 如果保存了大量不再需要的数据,可能增加内存占用
  • 逻辑复杂时会增加理解成本

所以闭包要合理使用,不要为了“炫技”而使用。


七、总结

闭包的本质并不神秘,可以简单记住一句话:

闭包就是函数可以“记住”并访问它定义时所在作用域中的变量。

它最常见的用途包括:

  • 保存状态
  • 私有变量
  • 回调场景
  • 模块化封装