JavaScript 开发者可能都听说过这个词。当我开始使用 JavaScript 时,经常会遇到闭包。我觉得它是 JavaScript 中最重要和最有趣的概念之一。

在本文中,我将尝试用一种有趣的方式来解释闭包。

在讲闭包之前,我们先了解词法作用域。如果你已经知道了,可以跳过下一部分。

词法作用域


你可能在想:我已经知道了局部和全局范围,词汇作用域是什么?我刚听到这个术语的时候,反应是一样的。别担心!让我们仔细看看。

其实它跟其他两个作用域一样简单。

function greetCustomer() {
    var customerName = "anchal";
    function greetingMsg() {
	  console.log("Hi! " + customerName); // Hi! anchal
    }
   greetingMsg();
}

从上面的输出可以看出,内部函数可以访问外部函数的变量。这是一个词法作用域,其中变量的作用域和值由定义/创建的位置(即代码中的位置)决定。你明白了吗?

我知道最后一点可能会让你感到困惑。我讲得详细点。你知道词法作用域的别称是静态作用域吗?

还有动态作用域,一些编程语言支持的。为什么我要提到动态作用域呢?因为它可以帮助你更好地理解词法作用域。

我们看看一些例子:

function greetingMsg() {
  console.log(customerName);// ReferenceError: customerName is not defined
}

function greetCustomer() {
   var customerName = "anchal";
   greetingMsg();
}

greetCustomer();

你同意这个输出吗?是的,会输出指代错误。这是因为这两个函数不能访问彼此的作用域,因为它们是分别定义的。

我们看看另一个例子:

function addNumbers(number1) {
  console.log(number1 + number2);
}

function addNumbersGenerate() {
  var number2 = 10;
  addNumbers(number2);
}

addNumbersGenerate();

对于有动态作用域的的语言,以上输出为 20。支持词法作用域的语言输出referenceError: number2 is not defined。为什么呢?

因为在动态作用域中,搜索首先在本地函数中进行,然后进入调用本地函数的 A 函数,然后在调用 A 函数的函数中进行搜索......

其名字不言自明——动态意味着变化。变量的作用域和值可以不同,这取决于调用函数的位置。变量的含义在运行时可以改变。

明白动态作用域的要点了吗?如果明白了,请记住词法作用域是相反的。

在词法作用域中,搜索首先在局部函数中进行,然后进入定义该函数的 A 函数中,然后在定义 A 函数的函数中进行搜索,依次类推。

因此,词法或静态作用域的意思是从定义变量的位置确定变量的作用域和值。

再看上面的例子,试着找到输出结果。做个小测试,在开头声明 number2

var number2 = 2;
function addNumbers(number1) {
  console.log(number1 + number2);
}

function addNumbersGenerate() {
  var number2 = 10;
  addNumbers(number2);
}

addNumbersGenerate();

你知道输出是什么吗?

对的,输出是 12。这是因为,首先看 addNumbers 函数(最内部的作用域),然后向内搜索,其中定义了这个函数。运行到 number2 变量时,输出为 12。

你可能在想为什么我在这里花了这么多时间解释词汇作用域。这是一篇关于闭包的文章,而不是关于词法作用域的文章。但是,如果不知道词法作用域,就不能理解闭包。

当我们看闭包的定义时,你就会明白。现在我们开始讲闭包。

什么是闭包


我们看看闭包的定义:

当一个内部函数可以访问外部函数的变量和参数时,闭包就形成了。内部函数可以访问——
1. 自己的变量
2. 外部函数的变量和参数
3. 全局变量

等等!这是闭包的定义还是词汇作用域的定义?两者的定义看起来一样,它们有什么区别?

这就是为什么我在前面讲词汇作用域的定义。闭包与词汇/静态作用域有关。

我们再看看另一个定义,了解闭包有什么不同。

闭包是指某个函数可以访问其词法作用域,即使该函数在其词法作用域外执行。

或者,

内部函数可以访问其父级作用域,即使父级函数已经执行。

如果你还没有理解这一点,请不要担心。下面的例子可以帮助你更好地理解。我们修改一下词法作用域的第一个例子:

function greetCustomer() {
  const customerName = "anchal";
  function greetingMsg() {
    console.log("Hi! " + customerName);
  }
  return greetingMsg;
}

const callGreetCustomer = greetCustomer();
callGreetCustomer(); // output – Hi! anchal

这个代码的区别在于我们回到内部函数,然后执行。在一些编程语言中,局部变量存在于函数执行期间。但是,函数执行之后,这些局部变量就不存在,无法访问。

但是,这里的场景是不同的。父函数执行后,内部函数(返回函数)仍可访问父函数的变量。是的,这就是闭包。

内部函数在执行父函数时会保留其词法作用域,因此内部函数可以访问这些变量。

为了更好地理解,我们使用控制台的 dir() 方法查看 callGretCustomer 属性列表:

从上面的图片可以看出,执行 greetCustomer() 时,内部函数如何保留父级作用域(customerName)。

“闭包是一个函数,包含在创建闭包时处于作用域内的所有变量或其他函数。在 JavaScript 中,闭包通过‘内部函数’的形式来实现,也就是在另一函数的主体内定义的函数。”

希望这个例子能够帮助大家更好地了解闭包的定义。现在你可能发现闭包挺有趣的,
接下来我们通过不同的示例,使这个话题变得更加有趣。

闭包使用示例

function counter() {
  let count = 0;
  return function() {
    return count++;
  };
}

const countValue = counter();
countValue(); // 0
countValue(); // 1
countValue(); // 2

每次调用countValue ,count 变量的值都会增加 1。等等——你是否认为 count 的值为 0?

嗯,这是错误的,因为闭包不能与值一起使用。它存储变量引用。这就是为什么当我们更新值时,它会在第二个或第三个调用中反映出来,依次类推。

现在感觉更清晰了吗? 让我们看另一个例子:

function counter() {
  let count = 0;
  return function () {
    return count++;
  };
}

const countValue1 = counter();
const countValue2 = counter();
countValue1();  // 0
countValue1();  // 1
countValue2();   // 0
countValue2();   // 1

希望你猜对了答案。如果没有,这就是原因:countValue1countValue2 都保留了自己的词法作用域。 他们有独立的词法作用域。在这两种情况下,都可以使用dir()检查[[scopes]]值。

让我们看第三个例子。

这个有点不同。在其中,我们必须编写一个函数来实现输出:

const addNumberCall = addNumber(7);
addNumberCall(8) // 15
addNumberCall(6) // 13

简单。使用你新获得的闭包知识:

function addNumber(number1) {
  return function (number2) {
    return number1 + number2;
  };
}

现在让我们看一些比较有意思的例子:

function countTheNumber() {
  var arrToStore = [];
  for (var x = 0; x < 9; x++) {
    arrToStore[x] = function () {
      return x;
    };
  }
  return arrToStore;
}

const callInnerFunctions = countTheNumber();
callInnerFunctions[0]() // 9
callInnerFunctions[1]() // 9

每个存储函数的数组元素都将输出 9。你猜对了吗? 希望如此,但还是让我告诉你原因吧——这是因为闭包。

闭包存储引用 ,而不是值。循环第一次运行时,x 的值为 0。然后第二次循环 x 为 1,依次类推。因为闭包存储引用,所以每次循环运行时,它都会更改 x 的值。最后,x 的值为 9。因此callInnerFunctions[0]()的输出为 9。

但是,如果你想要 0 到 8 的输出,怎么办? 简单! 使用闭包。

在查看以下解决方案之前,请考虑一下:

function callTheNumber() {
  function getAllNumbers(number) {
    return function() {
      return number;
    };
  }
  var arrToStore = [];
  for (var x = 0; x < 9; x++) {
    arrToStore[x] = getAllNumbers(x);
  }
  return arrToStore;
}

const callInnerFunctions = callTheNumber();
console.log(callInnerFunctions[0]()); // 0
console.log(callInnerFunctions[1]()); // 1

在这里,我们为每次迭代创建了单独的作用域。你可以使用console.dir(arrToStore)检查[[scopes]]中 x 的值,以获取不同的数组元素。

好啦,我希望你现在可以说你发现闭包很有趣。