网站

闭包 - JavaScript | MDN

函数 - JavaScript | MDN

引入

闭包用来解决一个问题:函数执行完之后,有些变量还想继续被后面的代码使用。

简单理解,闭包就是函数记住了它创建时所在作用域里的变量

正文

定义

定义

闭包是指一个函数可以访问并记住它外层函数作用域中的变量,即使外层函数已经执行结束。

闭包通常出现在:函数里面返回另一个函数

function outer() {
  const message = "你好";
 
  function inner() {
    console.log(message);
  }
 
  return inner;
}
 
const fn = outer();
 
fn();

这里 outer() 执行完之后,message 按理说应该结束了。

但是 inner() 还在使用 message,所以 message 被保留下来了。

这就是闭包。

语法

闭包最常见的结构是:外层函数里定义变量,再返回一个内部函数。

function outer() {
  let count = 0;
 
  return function () {
    count = count + 1;
    console.log(count);
  };
}
 
const add = outer();
 
add();
add();
add();

输出结果:

1
2
3

count 没有随着 outer() 执行结束而消失,因为返回出去的函数还在使用它。

为什么需要闭包

闭包常用来保存一些不想直接暴露到外部的数据。

function createCounter() {
  let count = 0;
 
  return function () {
    count = count + 1;
    return count;
  };
}
 
const counter = createCounter();
 
console.log(counter());
console.log(counter());

这里的 count 只能通过 counter() 修改,外部不能直接改它。

这样可以让数据更安全,不容易被随便修改。

常见用法

用法作用
保存变量状态让变量在函数执行结束后继续存在
封装私有数据外部不能直接访问内部变量
创建计数器每次调用时保留上一次的结果
配合回调函数回调执行时还能访问外层变量
函数工厂根据不同参数生成不同函数

例子

创建计数器

这个例子中,count 会一直被内部函数记住。

function createCounter() {
  let count = 0;
 
  return function () {
    count = count + 1;
    console.log(count);
  };
}
 
const counter = createCounter();
 
counter();
counter();
counter();

countcreateCounter() 里的局部变量,外部不能直接访问。

但是返回出来的函数可以继续使用它。

保存不同的配置

这个例子中,外层函数接收 name,内部函数会记住这个 name

function createGreeter(name) {
  return function () {
    console.log("你好," + name);
  };
}
 
const greetTom = createGreeter("Tom");
const greetJack = createGreeter("Jack");
 
greetTom();
greetJack();

greetTom() 记住的是 Tom

greetJack() 记住的是 Jack

它们互不影响。

配合事件使用

闭包也经常出现在事件回调里。

<button id="btn">点击</button>
function bindClick() {
  let count = 0;
  const btn = document.querySelector("#btn");
 
  btn.addEventListener("click", function () {
    count = count + 1;
    console.log("点击次数:" + count);
  });
}
 
bindClick();

bindClick() 执行结束后,点击事件里的函数还能继续使用 count

这也是闭包。

特点

  • 闭包可以让函数记住外层作用域里的变量
  • 外层函数执行结束后,被内部函数使用的变量不会马上销毁
  • 闭包常见写法是函数里返回函数
  • 闭包可以用来保存状态
  • 闭包可以用来封装私有变量
  • 闭包用多了可能导致变量一直被占用,不需要时要注意释放引用

常见错误

以为局部变量一定会消失

普通情况下,函数执行完,局部变量就不能再访问。

function test() {
  const name = "Tom";
}
 
test();
 
console.log(name);

上面这样访问不到 name

但如果内部函数还在使用这个变量,它就可能被保留下来。

function test() {
  const name = "Tom";
 
  return function () {
    console.log(name);
  };
}
 
const fn = test();
 
fn();

循环中使用闭包时结果不符合预期

使用 var 时,循环里的函数可能会共享同一个变量。

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

输出结果可能是:

3
3
3

因为 var 没有块级作用域,三个回调函数共享同一个 i

更推荐使用 let

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

输出结果:

0
1
2

let 每次循环都会形成新的块级作用域,所以每个函数记住的是不同的 i

理解

可以把闭包理解为:函数带着一个小背包

这个背包里装着它创建时能访问到的变量。

即使外层函数已经执行完了,只要内部函数还在,这些被用到的变量就还会被记住。

闭包不是一个新的语法,而是一种函数和作用域结合后产生的现象。

重点不是死记定义,而是看懂:哪个函数记住了哪个变量。

引出

理解闭包之后,可以继续学习 JS 作用域,知道变量到底在哪些位置可以被访问。

后续也可以结合 JS 函数调用方式JS this,理解函数执行时的上下文变化。