六维教程

JavaScript 闭包入门

上一篇讲过作用域,函数内部的变量在函数执行完后就销毁了。但有一种特殊情况,函数内部返回的函数还能访问外层函数的变量,即使外层函数早就执行完毕。这个机制就叫闭包,是 JavaScript 最经典也最容易绕晕的知识点。这篇用入门的方式讲清楚它是什么,以及最典型的两个用途。

理解了JavaScript 作用域再看闭包会轻松很多,作用域链是闭包的地基。

闭包是什么

先看一个现象。函数内定义一个函数并返回它,内层函数用到了外层函数的变量。

function outer() {
  let count = 0; // 外层函数的变量

  function inner() {
    count = count + 1; // 内层函数修改外层变量
    console.log(count);
  }

  return inner;
}

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

按上一篇的知识,outer 执行完,count 应该被销毁才对。但输出显示 count 一直在累加,从 1 到 2 到 3。原因就是闭包。inner 在诞生时把外层作用域一起记住了,outer 返回后,count 并没有销毁,而是被 inner 一直握着。

闭包的定义可以概括为,函数记住它诞生时所在的作用域,即使外层函数执行完毕,内层函数仍然能访问外层函数的变量。

计数器示例

上面这个例子就是一个计数器。count 存在闭包里,每次调用 fn 都在原来的基础上加 1。注意 count 是普通变量而不是函数参数,它的值在多次调用之间保持连续。

function makeCounter() {
  let count = 0;

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

const counter = makeCounter();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3

表面上看这像”函数里声明个变量再自增”,但注意每次调用 count 仍然活着,这正是闭包的效果。如果没有闭包,count 在函数第一次执行完就该消失。

闭包的用途之一,数据私有

直接声明一个全局变量做计数器,页面任何代码都能改它,容易出错。用闭包可以把计数变量藏起来,外部只能通过返回的函数操作它,不能直接改 count。

function makeCounter() {
  let count = 0;

  return {
    add: function () {
      count = count + 1;
    },
    get: function () {
      return count;
    }
  };
}

const counter = makeCounter();
counter.add();
counter.add();
console.log(counter.get()); // 输出 2
console.log(counter.count); // 输出 undefined,外部碰不到 count

counter.count 是 undefined,说明 count 完全藏在闭包内部,外部只能通过 add 和 get 间接使用。这个数据私有能力是闭包最常用的场景。

闭包的用途之二,工厂函数

makeCounter 每次调用都会创建一个全新的闭包,各自的 count 互不干扰。像这样生产函数的函数叫工厂函数。

function makeCounter() {
  let count = 0;

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

const counterA = makeCounter();
const counterB = makeCounter();

counterA(); // 返回 1
counterA(); // 返回 2
counterB(); // 返回 1,B 自己从 0 开始

counterA 和 counterB 是两个独立的闭包,各自记住各自的 count,互不影响。工厂函数很适合生成结构相似但状态独立的工具函数。

一个经典陷阱的说明

闭包和循环结合曾经有个经典陷阱。ES6 之前用 var 声明循环变量,多个闭包会共享同一个变量,导致取到的值相同。现在用 let 声明循环变量,每次迭代都是独立的绑定,这个问题基本消失。这个细节在 for 循环那篇提过,更深入的分析留到 ES6 教程再讲,这里只需记住用 let 就能绕开老陷阱。

小结

  • 闭包是函数记住诞生时作用域的机制,外层函数执行完毕,内层函数仍然能访问外层变量。
  • 闭包最常见的两个用途是数据私有和工厂函数,外层变量被封装,只能通过返回的函数操作。
  • 每次调用外层函数都会产生独立的闭包,互不干扰。
  • 闭包和循环的经典陷阱已被 let 的块级作用域解决,详细内容留到 ES6 阶段。
上一篇
JavaScript 函数表达式与箭头函数
下一篇
JavaScript 数组基本操作