Skip to content

JavaScript 中的闭包

给出 JavaScript 中闭包的定义,并通过示例来理解它。

本文转载自知乎回答

原问题如下:

在 js 中,当函数执行时,它会被销毁,相应地,这个函数的作用域也会被销毁。闭包的形式就是在函数中返回函数,然后用一个全局变量来接收返回的函数,因为全局变量不会被销毁,而这个变量保存了返回函数的地址,所以我们可以使用这个返回函数,因为只要函数内部使用了变量或其他东西,那么这个函数就不会被销毁。所以当我们在全局作用域中使用返回的函数时,主函数就不会被销毁,作用域自然存在,我们就可以自然地使用函数中的变量,并且能够保存状态。不知道我这样理解对不对?


以下是原回答

闭包的形式就是在函数中返回函数,然后用一个全局变量来接收返回的函数,因为全局变量不会被销毁,而这个变量保存了返回函数的地址,所以我们可以使用这个返回函数,因为只要函数内部使用了变量或其他东西,那么这个函数就不会被销毁。所以当我们在全局作用域中使用返回的函数时,主函数就不会被销毁,作用域自然存在,我们就可以自然地使用函数中的变量,并且能够保存状态。

你说了一大段话,那我先试着拆解一下你的理解,再慢慢分析。

你说

闭包的形式就是函数中返回函数

这是不对的。JavaScript 在设计之初就考虑了函数式编程,函数是这门语言中的一等公民。这意味着函数可以像其他类型一样存储在变量中、作为参数传给其他函数,或者作为函数执行的返回值返回。函数返回函数,这是由语言范式决定的,与闭包无关。

所以要理解闭包的形式,必须从定义入手。下面直接给出它的定义:

当一个函数在其定义时的词法作用域之外执行,并且仍然能够记住并访问其定义时的词法作用域时,就产生了闭包。

根据上述定义,我们可以知道闭包的两个特征:

  • 第一,它在定义时的词法作用域之外执行。
  • 第二,它还能访问定义时的词法作用域。

这样说起来有些绕口,那我们就举个示例来说明。

第一个示例:

function fpp() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  return bar;
}

var baz = foo(); // 嘿,这就是闭包的效果

bar 函数的词法作用域在 foo 函数内部。但如果在 foo 函数外部直接调用 bar 函数,编译器在 foo 外部找不到 bar 的定义,因此不识别 bar,会抛出错误,提示 bar 未定义。所以如果要在 foo 函数外部执行 bar 函数,可以把 bar 本身作为返回值,让 foo 外部的 baz 变量接收 bar 函数,再执行它。这样,编译器就能知道 foo 生成之后 bar 存储在 baz 中,从而明确 bar 的身份:它是一个函数,可以在 foo 外部执行。

但根据 JavaScript 的语言设定,函数在执行时可以访问它所在的词法作用域。例如,全局作用域中的函数可以访问全局变量。同理,bar 函数在被声明时,可以访问它所在的作用域,即 foo 函数内部。而 foo 函数的作用域中有一个变量 a,那么这个变量 abar 函数执行时可以被直接访问。

但这不是在讲函数返回函数吗?那就再看第二个示例

function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  bar(baz);
}

function bar(fn) {
  fn();
}

bar 函数声明在 foo 外部,baz 定义在 foo 内部,而 foo 函数可以访问 bar。现在把 baz 传给 bar 函数,bar 接收后就会执行它。此时 baz 通过传参被转移到 bar 中的作用域执行,并且能够访问原作用域中的 a,这就是闭包。而且它并不涉及函数返回函数的问题。

你看,在上面第一个示例中,bar 函数被存储到词法作用域之外的 baz 变量后执行,仍然能够访问词法作用域中的变量 a,这就产生了闭包。而在第二个示例中,baz 通过参数传给 bar,从而在作用域之外执行,并且能够访问作用域内的 a。这也是闭包最本质的特征。

你说

因为全局变量不会被销毁,而这个变量保存了返回函数的地址,所以我们可以使用这个返回函数,因为只要函数内部使用了变量或其他东西,那么这个函数就不会被销毁。

你说全局变量不会被销毁,这点是对的。但我不太明白你想表达什么,也许你想说的是,只要一个函数的引用没有被销毁,那么这个函数就不会被销毁?然而,无论如何,全局变量会不会被销毁,与闭包会不会被销毁毫无关系……

在上面第一个示例中,baz 定义在全局,存储的是对 bar 的引用。baz 因定义在全局而不会被销毁,而 barfoo 是否会被销毁其实无所谓。

所以回到第二个示例

function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  bar(baz);
}

function bar(fn) {
  fn();
}

bar 函数声明在 foo 外部,baz 定义在 foo 内部,foo 函数可以访问 bar。现在把 baz 传给 bar 函数,bar 接收后执行它。此时 baz 通过传参被转移到 bar 中的作用域执行,并能访问原作用域中的 a,这就是闭包。在这里,baz 是在 bar 的作用域中执行的,而不是在全局,全局中也没有对 baz 的引用。但这也是闭包,所以 baz 同样不会被销毁。

闭包不会被销毁,这是由 JavaScript 解释器决定的。当解释器在执行过程中检测到产生了闭包,闭包所在的词法作用域就会被解释器圈起来,永远不会被销毁。而不是因为闭包被全局引用、不会被销毁,所以闭包才不会被销毁。这与全局作用域毫无关系。

理解闭包是一个费力的过程。这方面有很多坑。计算机编程的术语比较抽象。因此,建议你在接触一个概念时,最好先从定义入手,再结合具体示例,而不是直接从示例开始。你所说的「函数返回函数」,很可能就是受到网上一些教程直接抛出示例的影响。

希望我的回答对你有所帮助。