JavaScript 闭包的简单解释

720 阅读7分钟

由于闭包,回调、事件处理程序、高阶函数可以访问外部作用域变量。闭包在函数式编程中很重要,在 JavaScript 编码面试中经常被问到。

虽然到处都在使用,但闭包很难掌握。如果你还没有“啊哈!” 理解闭包的时刻,那么这篇文章适合你。

我将从基本术语开始:作用域和词法作用域。然后,在掌握了基础知识之后,您只需一步即可最终理解闭包。

在开始之前,我建议您抵制跳过作用域和词法作用域部分的冲动。这些概念对于闭包至关重要,如果你掌握了它们,闭包的想法就变得不言而喻了。

一、作用域

当您定义一个变量时,您希望它存在于某些边界内。例如,result变量calculate()作为内部细节存在于函数中是有意义的。在 之外calculate(),该result变量是无用的。

变量的可访问性由作用域管理。您可以自由访问在其作用域内定义的变量。但是在该作用域之外,该变量是不可访问的。

在 JavaScript 中,作用域由函数或代码块创建。

让我们看看作用域如何影响变量的可用性count。此变量属于 function 创建的作用域foo()

function foo() {
  // The function scope
  let count = 0;
  console.log(count); // logs 0
}

foo();
console.log(count); // ReferenceError: count is not defined

尝试演示。

count作用域内可以自由访问foo()

但是,在foo()作用域之外,count是无法访问的。如果您count无论如何都尝试从外部访问,JavaScript 会抛出ReferenceError: count is not defined.

如果您在函数或代码块内定义了一个变量,那么您只能在该函数或代码块内使用该变量。上面的示例演示了这种行为。

JavaScript 作用域

现在,让我们看一个通用的公式:

作用域是一个空间策略,它规定了变量的可访问性。

一个直接的特性出现了——作用域隔离了变量。这很好,因为不同的作用域可以有同名的变量

您可以重用公共变量的名称(countindexcurrentvalue在不同的作用域,等等)不冲突。

foo()bar()函数作用域有自己的但同名的变量count

function foo() {
  // "foo" function scope
  let count = 0;
  console.log(count); // logs 0
}

function bar() {
  // "bar" function scope
  let count = 1;
  console.log(count); // logs 1
}

foo();
bar();

尝试演示。

count来自foo()bar()函数作用域的变量不会发生冲突。

2. 作用域嵌套

让我们更多地使用作用域,并将一个作用域嵌套到另一个作用域中。例如,函数innerFunc()嵌套在外部函数中outerFunc()

2 个函数作用域将如何相互交互?我可以访问变量outerVarouterFunc()从内部innerFunc()作用域?

让我们在示例中尝试一下:

function outerFunc() {
  // the outer scope
  let outerVar = 'I am outside!';

  function innerFunc() {
    // the inner scope
    console.log(outerVar); // => logs "I am outside!"
  }

  innerFunc();
}

outerFunc();

尝试演示。

事实上,outerVar变量在innerFunc()作用域内是可以访问的。外部作用域的变量可以在内部作用域内访问。

JavaScript 作用域可以嵌套

现在你知道了两件有趣的事情:

  • 作用域可以嵌套
  • 外部作用域的变量可以在内部作用域内访问

3. 词法作用域

如何JavaScript的理解outerVar里面innerFunc()相当于可变outerVarouterFunc()

JavaScript 实现了一种名为词法作用域(或静态作用域)的作用域机制。词法作用域意味着变量的可访问性由嵌套作用域内变量的位置决定。

更简单的是,词法作用域意味着在内部作用域内您可以访问外部作用域的变量。

之所以称为词法(或静态),是因为引擎仅通过查看 JavaScript 源代码而不执行它来确定(在词法分析时)作用域的嵌套。

词法作用域的提炼思想:

词法作用域由静态确定的外部作用域组成。

例如:

const myGlobal = 0;

function func() {
  const myVar = 1;
  console.log(myGlobal); // logs "0"

  function innerOfFunc() {
    const myInnerVar = 2;
    console.log(myVar, myGlobal); // logs "1 0"

    function innerOfInnerOfFunc() {
      console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0"
    }

    innerOfInnerOfFunc();
  }

  innerOfFunc();
}

func();

尝试演示。

innerOfInnerOfFunc()的词法作用域由innerOfFunc()、func()和全局作用域(最外层作用域)组成。在innerOfInnerOfFunc()中,你可以访问词法范围变量myInnerVar、myVar和myGlobal。

innerFunc()的词法范围包括func()和全局范围。在innerOfFunc()中,你可以访问词法范围的变量myVar和myGlobal。

最后,func()的词法范围只包括全局范围。在func()中,你可以访问词法范围变量myGlobal。

4. 闭包

好的,词法作用域允许静态访问外部作用域的变量。距离闭包只差一步!

让我们再看一下outerFunc()innerFunc()示例:

function outerFunc() {
  let outerVar = 'I am outside!';

  function innerFunc() {
    console.log(outerVar); // => logs "I am outside!"
  }

  innerFunc();
}

outerFunc();

尝试演示。

innerFunc()作用域内,变量outerVar是从词法作用域访问的。这已经知道了。

请注意,innerFunc()调用发生在其词法范围( 的范围outerFunc())内。

让我们做一个改变:innerFunc()在它的词法范围之外调用:在一个函数中exec()。将innerFunc()仍然能够访问outerVar

让我们对代码片段进行调整:

function outerFunc() {
  let outerVar = 'I am outside!';

  function innerFunc() {
    console.log(outerVar); // => logs "I am outside!"
  }

  return innerFunc;
}

function exec() {
  const myInnerFunc = outerFunc();
  myInnerFunc();
}

exec();

尝试演示。

现在innerFunc()在其词法范围之外执行,但恰好在exec()`函数范围内。重要的是:

innerFunc()仍然可以访问outerVar其词法范围,即使在其词法范围之外执行。

换句话说,从其词法作用域中innerFunc() 闭包(也就是捕获、记住)变量outerVar

换句话说,innerFunc()是一个*闭包,*因为它outerVar从它的词法范围闭包了变量。

JavaScript 闭包

您已经完成了了解闭包是什么的最后一步:

闭包是一个访问其词法范围甚至在其词法范围之外执行的函数。

更简单的是,闭包是一个函数,它从定义它的地方记住变量,而不管它后面在哪里执行。

识别闭包的经验法则:如果在函数内部看到一个外来变量(未在该函数内定义),则该函数很可能是一个闭包,因为外来变量被捕获。

在前面的代码片段中,outerVarinnerFunc()outerFunc()作用域捕获的闭包内的一个外来变量。

让我们继续举例说明为什么闭包是有用的。

5. 闭包示例

5.1 事件处理程序

让我们显示一个按钮被点击的次数:

let countClicked = 0;

myButton.addEventListener('click', function handleClick() {
  countClicked++;
  myText.innerText = `You clicked ${countClicked} times`;
});

打开演示并单击按钮。文本会更新以显示点击次数。

单击按钮时,handleClick()会在 DOM 代码内部的某处执行。执行发生在远离定义的地方。

但是作为一个闭包,它从词法作用域中handleClick()捕获countClicked并在点击发生时更新它。更何况,myText也被俘虏了。

5.2 回调

从词法范围捕获变量在回调中很有用。

一个setTimeout()回调函数:

const message = 'Hello, World!';

setTimeout(function callback() {
  console.log(message); // logs "Hello, World!"
}, 1000);

Thecallback()是一个闭包,因为它捕获了变量message

forEach()的迭代器函数:

let countEven = 0;
const items = [1, 5, 100, 10];

items.forEach(function iterator(number) {
  if (number % 2 === 0) {
    countEven++;
  }
});

countEven; // => 2

尝试演示。

iterator是一个闭包,因为它捕获countEven变量。

5.3 函数式编程

当一个函数返回另一个函数直到参数被完全提供时,就会发生柯里化。

例如:

function multiply(a) {
  return function executeMultiply(b) {
    return a * b;
  }
}

const double = multiply(2);
double(3); // => 6
double(5); // => 10

const triple = multiply(3);
triple(4); // => 12

尝试演示。

multiply 是一个柯里化函数,它返回另一个函数。

柯里化是函数式编程的一个重要概念,由于闭包,它也成为可能。

executeMultiply(b)是一个a从其词法范围

六,结论

范围规则变量的可访问性。可以有一个函数或一个块作用域。

词法作用域允许函数作用域静态访问来自外部作用域的变量。

最后,闭包是一个从其词法范围捕获变量的函数。简单来说,闭包从定义它的地方开始记住变量,不管它在哪里执行。

闭包允许事件处理程序、回调来捕获变量。它们用于函数式编程。此外,在前端工作面试期间,您可能会被问及关闭如何运作。

每个 JavaScript 开发人员都必须知道闭包是如何工作的。处理它⌐■_■。

你还在为理解闭包而苦恼吗?如果是这样,请在下面的评论中问我一个问题!