什么是'关闭'?

Ben*_*Ben 403 computer-science glossary functional-programming terminology

我问了一个关于Currying和关闭的问题.什么是关闭?它与currying有什么关系?

sup*_*ary 680

可变范围

声明局部变量时,该变量具有范围.通常,局部变量仅存在于声明它们的块或函数中.

function() {
  var a = 1;
  console.log(a); // works
}    
console.log(a); // fails
Run Code Online (Sandbox Code Playgroud)

如果我尝试访问本地变量,大多数语言将在当前作用域中查找它,然后通过父作用域查找它们,直到它们到达根作用域.

var a = 1;
function() {
  console.log(a); // works
}    
console.log(a); // works
Run Code Online (Sandbox Code Playgroud)

使用块或函数时,不再需要其局部变量,并且通常会将内存变量耗尽.

这就是我们通常期望事情发挥作用的方式.

闭包是一个持久的局部变量范围

闭包是一个持久的作用域,即使在代码执行移出该块之后也会保留局部变量.支持闭包的语言(例如JavaScript,Swift和Ruby)将允许您保持对范围(包括其父范围)的引用,即使在声明这些变量的块已经完成执行之后,只要您保留引用某个地方的那个街区或功能.

scope对象及其所有局部变量都与函数绑定,只要该函数持续存在,它就会持久存在.

这为我们提供了功能可移植性.当我们稍后调用函数时,即使我们在完全不同的上下文中调用函数,我们也可以期望在函数首次定义时仍在范围内的任何变量.

例如

这是JavaScript中一个非常简单的例子,说明了这一点:

outer = function() {
  var a = 1;
  var inner = function() {
    console.log(a);
  }
  return inner; // this returns a function
}

var fnc = outer(); // execute outer to get inner 
fnc();
Run Code Online (Sandbox Code Playgroud)

这里我在函数中定义了一个函数.内部函数可以访问所有外部函数的局部变量,包括a.变量a在内部函数的范围内.

通常,当函数退出时,其所有局部变量都会被吹走.但是,如果我们返回内部函数并将其分配给变量,fnc以便在outer退出后它仍然存在,那么inner定义范围内的所有变量也会保持不变.该变量a已被关闭 - 它在一个闭包内.

请注意,该变量a完全是私有的fnc.这是一种在JavaScript等函数式编程语言中创建私有变量的方法.

正如你可能猜到的那样,当我调用fnc()它时会打印出值a"1".

在没有闭包的语言中,变量a将被垃圾收集并在函数outer退出时被丢弃.调用fnc会抛出错误,因为a不再存在.

在JavaScript中,变量a持久存在,因为变量作用域是在首次声明函数时创建的,并且只要函数继续存在就会持续存在.

a属于范围outer.范围inner有一个指向范围的父指针outer.fnc是一个指向的变量inner.a只要fnc坚持下去就坚持下去.a在封闭范围内.

  • 我认为这是一个非常好且易于理解的例子. (105认同)
  • 感谢您的精彩解释,我见过很多,但这是我真正得到它的时间. (14认同)
  • 嗨Jubbat,是的,打开jquery.js并看看第一行.你会看到一个功能被打开了.现在跳到最后,你会看到window.jQuery = window.$ = jQuery.然后该功能关闭并自行执行.您现在可以访问$函数,该函数又可以访问闭包中定义的其他函数.这是否回答你的问题? (5认同)
  • 在网上的最佳解释.比我想象的简单 (4认同)
  • 我已经阅读了关于这个主题的教科书两天了,但无法真正理解发生了什么。阅读您的答案花了 4 分钟,这是非常有意义的。 (3认同)
  • 我可以举一个如何在像JQuery这样的库中工作的例子,如第2到第2段所述?我并不完全明白. (2认同)
  • 代表来自废弃的Objective-C之地的新Swift社区发言:谢谢.这个答案很完美. (2认同)
  • 这就是为什么我比Wiki更喜欢这样的原因 (2认同)
  • @BlissRage - 主要目的之一是用于事件处理程序。当您设置处理程序时,您可以访问一堆局部变量。但稍后,当调用处理程序时,这些变量可能已更改或可能不再存在。Closure 为您提供了一个可靠的运行时环境。 (2认同)

Kyl*_*nin 91

我将给出一个示例(在JavaScript中):

function makeCounter () {
  var count = 0;
  return function () {
    count += 1;
    return count;
  }
}

var x = makeCounter();

x(); returns 1

x(); returns 2

...etc...
Run Code Online (Sandbox Code Playgroud)

makeCounter这个函数的作用是返回一个函数,我们称之为x,每次调用它时都会计数一次.由于我们没有向x提供任何参数,因此它必须以某种方式记住计数.它知道在何处根据所谓的词法范围找到它 - 它必须查找定义为找到值的位置.这个"隐藏"值就是所谓的闭包.

这是我再次讨论的例子:

function add (a) {
  return function (b) {
    return a + b;
  }
}

var add3 = add(3);

add3(4); returns 7
Run Code Online (Sandbox Code Playgroud)

您可以看到,当您使用参数a(为3)调用add时,该值包含在我们定义为add3的返回函数的闭包中.这样,当我们调用add3时,它知道在哪里找到一个值来执行添加.

  • IDK,您在上述语言中使用的语言(可能是F#).可以在伪代码中给出上面的例子吗?我很难理解这一点. (4认同)
  • @KyleCronin很好的例子,谢谢.问:说"隐藏值被称为闭包"或者"隐藏价值的函数是闭包"更正确吗?或者"隐藏价值的过程是封闭"?谢谢! (3认同)
  • @RobertHume好问题.在语义上,术语"封闭"有点含糊不清.我的个人定义是隐藏值和封闭函数对它的使用的组合构成了闭包. (2认同)

Ben*_*lds 56

凯尔的答案非常好.我认为唯一的另一个澄清是闭包基本上是创建lambda函数时堆栈的快照.然后,当重新执行该函数时,堆栈将在执行该函数之前恢复到该状态.因此,正如凯尔所提到的,count当lambda函数执行时,隐藏值()是可用的.

  • 它不仅仅是堆栈 - 它是保留的封闭词法范围,无论它们是存储在堆栈还是堆栈(或两者). (14认同)

Sas*_*asQ 30

首先,与大多数人告诉你的相反,封闭不是一个功能!那什么?
它是在函数的"周围上下文"(称为其环境)中定义的一符号,它使其成为CLOSED表达式(即,每个符号都被定义并具有值的表达式,因此可以对其进行求值).

例如,当你有一个JavaScript函数时:

function closed(x) {
  return x + 3;
}
Run Code Online (Sandbox Code Playgroud)

它是一个封闭的表达式,因为它中出现的所有符号都在其中定义(它们的含义很清楚),因此您可以对其进行评估.换句话说,它是独立的.

但是如果你有这样的功能:

function open(x) {
  return x*y + 3;
}
Run Code Online (Sandbox Code Playgroud)

它是一个开放式表达式,因为其中有符号,但尚未在其中定义.即,y.在查看这个函数时,我们无法分辨y它是什么意思,它是什么意思,我们不知道它的价值,所以我们无法评估这个表达式.也就是说,在我们告诉y它应该意味着什么之前我们不能调用这个函数.这y称为自由变量.

y需要一个定义,但这个定义不是函数的一部分 - 它在其他地方,在其"周围环境"(也称为环境)中定义.至少这是我们所希望的:P

例如,它可以全局定义:

var y = 7;

function open(x) {
  return x*y + 3;
}
Run Code Online (Sandbox Code Playgroud)

或者它可以在包装它的函数中定义:

var global = 2;

function wrapper(y) {
  var w = "unused";

  return function(x) {
    return x*y + 3;
  }
}
Run Code Online (Sandbox Code Playgroud)

环境中给出表达式中的自由变量的部分是闭包.它以这种方式调用,因为它通过为所有自由变量提供这些缺少的定义,将一个开放表达式转换为一个闭合表达式,以便我们可以对它进行评估.

在上面的例子中,内部函数(我们没有给出名称因为我们不需要它)是一个开放表达式,因为y它中的变量是自由的 - 它的定义在函数之外,在包装它的函数中.该匿名函数的环境是变量集:

{
  global: 2,
  w: "unused",
  y: [whatever has been passed to that wrapper function as its parameter `y`]
}
Run Code Online (Sandbox Code Playgroud)

现在,闭包是这个环境的一部分,它通过提供所有自由变量的定义来关闭内部函数.在我们的例子中,内部函数中唯一的自由变量是,所以该函数的闭包是它的环境的子集:y

{
  y: [whatever has been passed to that wrapper function as its parameter `y`]
}
Run Code Online (Sandbox Code Playgroud)

环境中定义的其他两个符号不是该函数闭包的一部分,因为它不需要它们运行.他们不需要关闭它.

更多关于这背后的理论:https: //stackoverflow.com/a/36878651/434562

值得注意的是,在上面的示例中,包装函数将其内部函数作为值返回.从定义(或创建)函数的那一刻起,我们调用此函数的那一刻可以是远程的.特别是,它的包装函数不再运行,并且它在调用堆栈上的参数不再存在:P这会产生问题,因为内部函数y在调用时需要存在!换句话说,它需要来自其闭包的变量以某种方式包装函数更长,并在需要时存在.因此,内部函数必须创建这些变量的快照,这些变量使其闭合并将它们存储在安全的位置供以后使用.(在调用堆栈之外的某个地方.)

这就是为什么人们常常将闭包这个术语混淆为特殊类型的函数,它可以对它们使用的外部变量进行快照,或者用于存储这些变量的数据结构以供日后使用.但我希望您现在明白它们不是闭包本身 - 它们只是在编程语言中实现闭包的方法,或者是允许函数闭包的变量在需要时存在的语言机制.关于闭包存在很多误解,这些错误(不必要地)使得这个主题实际上更加混乱和复杂.

  • 多年来我读过很多关于闭包的定义,但我认为到目前为止我最喜欢这个。我想我们都有自己的方式来在心理上绘制这样的概念,而这个方式与我的非常吻合。 (5认同)
  • 我在google、youtube、书籍、博客等上看到了很多解释,它们都很有道理,很好,但我认为这是逻辑上最清晰的解释。 (3认同)
  • 一个可能对初学者有帮助的类比是一个闭包*把所有松散的末端*,这是一个人在*寻求闭包*(或者它*解决*所有必要的引用,或......)时所做的。好吧,它帮助我这样想:o) (2认同)

Joh*_*kin 28

闭包是一个可以引用另一个函数中的状态的函数.例如,在Python中,这使用闭包"inner":

def outer (a):
    b = "variable in outer()"
    def inner (c):
        print a, b, c
    return inner

# Now the return value from outer() can be saved for later
func = outer ("test")
func (1) # prints "test variable in outer() 1
Run Code Online (Sandbox Code Playgroud)


Kyl*_*nin 23

为了帮助理解闭包,检查它们如何以过程语言实现可能是有用的.这个解释将遵循Scheme中闭包的简单实现.

首先,我必须介绍命名空间的概念.当您向Scheme解释器输入命令时,它必须评估表达式中的各种符号并获取它们的值.例:

(define x 3)

(define y 4)

(+ x y) returns 7
Run Code Online (Sandbox Code Playgroud)

define表达式将值3存储在x的点中,将值4存储在y的点中.然后当我们调用(+ xy)时,解释器查找命名空间中的值并能够执行操作并返回7.

但是,在Scheme中有一些表达式允许您临时覆盖符号的值.这是一个例子:

(define x 3)

(define y 4)

(let ((x 5))
   (+ x y)) returns 9

x returns 3
Run Code Online (Sandbox Code Playgroud)

let关键字的作用是引入一个新的命名空间,其中x为值5.您将注意到它仍然能够看到y为4,使得返回的总和为9.您还可以看到表达式结束后x从这个意义上讲,x已经被本地值暂时屏蔽了.

程序和面向对象语言具有类似的概念.每当在函数中声明一个与全局变量同名的变量时,您都会获得相同的效果.

我们如何实现这个?一种简单的方法是使用链表 - 头部包含新值,尾部包含旧命名空间.当你需要查找符号时,从头部开始沿着尾部向下移动.

现在让我们暂时跳到一流函数的实现.或多或少,函数是一组在函数被调用时返回值最终执行的指令.当我们读入一个函数时,我们可以在幕后存储这些指令并在调用函数时运行它们.

(define x 3)

(define (plus-x y)
  (+ x y))

(let ((x 5))
  (plus-x 4)) returns ?
Run Code Online (Sandbox Code Playgroud)

我们将x定义为3,将plus-x定义为其参数y,加上x的值.最后,我们在一个x被新x掩盖的环境中调用plus-x,这个值为5.如果我们只是为函数plus-x存储操作(+ xy),因为我们在上下文中如果x为5,则返回的结果为9.这就是所谓的动态范围.

但是,Scheme,Common Lisp和许多其他语言都有所谓的词法作用域 - 除了存储操作(+ xy)之外,我们还将命名空间存储在该特定点.这样,当我们查找值时,我们可以看到x,在这个上下文中,实际上是3.这是一个闭包.

(define x 3)

(define (plus-x y)
  (+ x y))

(let ((x 5))
  (plus-x 4)) returns 7
Run Code Online (Sandbox Code Playgroud)

总之,我们可以使用链表来存储函数定义时命名空间的状态,允许我们从封闭范围访问变量,并使我们能够本地屏蔽变量而不影响其余部分.程序.


ada*_*Lev 10

这是一个真实世界的例子,说明为什么Closures会屁股......这是我的Javascript代码.让我来说明一下.

Function.prototype.delay = function(ms /*[, arg...]*/) {
  var fn = this,
      args = Array.prototype.slice.call(arguments, 1);

  return window.setTimeout(function() {
      return fn.apply(fn, args);
  }, ms);
};
Run Code Online (Sandbox Code Playgroud)

以下是您将如何使用它:

var startPlayback = function(track) {
  Player.play(track);  
};
startPlayback(someTrack);
Run Code Online (Sandbox Code Playgroud)

现在假设您希望播放延迟开始,例如在此代码段运行后5秒钟.好吧,这很简单,delay它的关闭:

startPlayback.delay(5000, someTrack);
// Keep going, do other things
Run Code Online (Sandbox Code Playgroud)

当您delay使用5000ms 调用时,第一个代码段会运行,并将传入的参数存储在它的闭包中.然后5秒后,当setTimeout回调发生时,闭包仍然保持这些变量,因此它可以用原始参数调用原始函数.
这是一种currying或功能装饰.

如果没有闭包,你必须以某种方式将这些变量保持在函数外部,从而在函数外部乱码,逻辑上属于它.使用闭包可以大大提高代码的质量和可读性.


tot*_*dli 7

TL;博士

闭包是一个函数,其范围分配给(或用作变量).因此,名称闭包:范围和函数被封闭和使用,就像任何其他实体一样.

深入维基百科的风格解释

根据维基百科,关闭是:

在具有第一类函数的语言中实现词法范围名称绑定的技术.

那是什么意思?让我们看看一些定义.

我将使用此示例解释闭包和其他相关定义:

function startAt(x) {
    return function (y) {
        return x + y;
    }
}

var closure1 = startAt(1);
var closure2 = startAt(5);

console.log(closure1(3)); // 4 (x == 1, y == 3)
console.log(closure2(3)); // 8 (x == 5, y == 3)
Run Code Online (Sandbox Code Playgroud)

一流的功能

基本上这意味着我们可以像任何其他实体一样使用函数.我们可以修改它们,将它们作为参数传递,从函数返回它们或为变量分配它们.从技术上讲,他们是一等公民,因此得名:一流的功能.

在上面的示例中,startAt返回一个(匿名)函数,该函数被赋值给closure1closure2.正如您所看到的,JavaScript就像其他任何实体(一等公民)一样对待函数.

名称绑定

名称绑定是关于找出变量(标识符)引用的数据.范围在这里非常重要,因为这将决定如何解决绑定.

在上面的例子中:

  • 在内部匿名函数的范围内,y是必然的3.
  • startAt范围内,x必然15(取决于封闭).

在匿名函数的作用域内,x不受任何值的约束,因此需要在upper(startAts)作用域中进行解析.

词汇范围

正如维基百科所说,范围:

绑定有效的计算机程序区域:名称可用于引用实体.

有两种技巧:

  • 词法(静态)范围:变量的定义通过搜索其包含的块或函数来解析,然后如果搜索外部包含块失败,依此类推.
  • 动态范围:搜索调用函数,然后调用调用函数的函数,依此类推,调用堆栈.

有关更多说明,请查看此问题查看Wikipedia.

在上面的示例中,我们可以看到JavaScript是词法范围的,因为在x解析时,将startAt根据源代码(在内部定义查找x的匿名函数)在上部(s)范围内搜索绑定,startAt并且不基于调用堆栈,调用函数的方式(范围).

包裹(闭合)

在我们的例子中,当我们调用startAt,它会返回将被分配到一个(第一类)功能closure1,并closure2因此关闭被创建,因为传递的变量15将内保存startAt的范围,将与返回的封闭匿名功能.当我们通过closure1closure2使用相同的参数(3)调用此匿名函数时,y将立即找到值(因为这是该函数的参数),但x不限于匿名函数的范围,因此分辨率继续(词法)上部函数作用域(保存在闭包中),其中x发现被绑定到1或者5.现在我们知道了总和的所有内容,因此可以返回结果,然后打印出来.

现在您应该了解闭包及其行为方式,这是JavaScript的基本组成部分.

哗众取宠

哦,你还学会了currying的内容:你使用函数(闭包)来传递一个操作的每个参数,而不是使用一个带有多个参数的函数.


sou*_*ogi 5

不包含自由变量的函数称为纯函数.

包含一个或多个自由变量的函数称为闭包.

var pure = function pure(x){
  return x 
  // only own environment is used
}

var foo = "bar"

var closure = function closure(){
  return foo 
  // foo is a free variable from the outer environment
}
Run Code Online (Sandbox Code Playgroud)

src:https://leanpub.com/javascriptallongesix/read#leanpub-auto-if-functions-without-free-variables-are-pure-are-closures-impure


小智 5

闭包是 JavaScript 中的一项功能,其中函数可以访问自己的作用域变量、外部函数变量和全局变量。

即使在外部函数返回之后,闭包也可以访问其外部函数作用域。这意味着即使在函数完成后,闭包也可以记住并访问其外部函数的变量和参数。

内部函数可以访问在它自己的作用域、外部函数的作用域和全局作用域中定义的变量。并且外层函数可以访问在其自身作用域和全局作用域中定义的变量。

关闭示例

var globalValue = 5;

function functOuter() {
  var outerFunctionValue = 10;

  //Inner function has access to the outer function value
  //and the global variables
  function functInner() {
    var innerFunctionValue = 5;
    alert(globalValue + outerFunctionValue + innerFunctionValue);
  }
  functInner();
}
functOuter();  
Run Code Online (Sandbox Code Playgroud)

输出将是其内部函数自身变量、外部函数变量和全局变量值之和的 20。