关于函数式编程:什么是’闭包’?

关于函数式编程:什么是’闭包’?

What is a 'Closure'?

我问了一个关于咖喱和闭包的问题。什么是关闭?它与咖喱有什么关系?


可变范围

当您声明一个局部变量时,该变量有一个作用域。通常,局部变量只存在于声明它们的块或函数中。

1
2
3
4
5
function() {
  var a = 1;
  console.log(a); // works
}    
console.log(a); // fails

如果我试图访问一个局部变量,大多数语言都会在当前范围内查找它,然后在父范围内查找,直到它们到达根范围为止。

1
2
3
4
5
var a = 1;
function() {
  console.log(a); // works
}    
console.log(a); // works

当一个块或函数完成时,它的局部变量就不再需要了,通常会耗尽内存。

这就是我们通常所期望的工作方式。

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

闭包是一个持久的作用域,即使在代码执行从该块中移出之后,它仍然保留着局部变量。支持闭包的语言(如javascript、swift和ruby)将允许您保留对某个范围(包括其父范围)的引用,即使在声明这些变量的块执行完毕之后,只要您在某个地方保留对该块或函数的引用。

作用域对象及其所有局部变量都与函数绑定在一起,并且只要该函数持续存在,它就会一直存在。

这给了我们功能可移植性。我们可以预期,当我们稍后调用函数时,在第一次定义函数时范围内的任何变量仍然在范围内,即使我们在完全不同的上下文中调用函数也是如此。

例如

下面是一个非常简单的javascript示例,说明了这一点:

1
2
3
4
5
6
7
8
9
10
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();

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

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

注意变量afnc是完全私有的。这是一种在函数式编程语言(如javascript)中创建私有变量的方法。

你也许可以猜到,当我调用fnc()时,它会打印a的值,即"1"。

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

在javascript中,变量a持续存在,因为变量范围是在首次声明函数时创建的,并且只要函数继续存在,变量范围就持续存在。

a属于outer的范围。inner的作用域有一个指向outer作用域的父指针。fnc是指向inner的变量。只要fnc持续,a就持续。a在关闭范围内。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function makeCounter () {
  var count = 0;
  return function () {
    count += 1;
    return count;
  }
}

var x = makeCounter();

x(); returns 1

x(); returns 2

...etc...

这个函数makecounter的作用是返回一个函数,我们称之为x,每次调用该函数时,该函数将按1计。因为我们没有给x提供任何参数,它必须记住计数。它知道在什么地方可以根据所谓的词法范围来找到它——它必须找到它定义的地方来找到值。这个"隐藏"值就是所谓的闭包。

下面是我的当前示例:

1
2
3
4
5
6
7
8
9
function add (a) {
  return function (b) {
    return a + b;
  }
}

var add3 = add(3);

add3(4); returns 7

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


凯尔的回答很好。我认为唯一的补充说明是闭包基本上是创建lambda函数时堆栈的快照。然后,当重新执行函数时,在执行函数之前,堆栈将恢复到该状态。因此,正如Kyle提到的,当lambda函数执行时,隐藏值(count可用。


首先,与这里大多数人告诉你的相反,闭包并不是一个功能!那是什么?< BR>它是在函数的"环境上下文"(称为其环境)中定义的一组符号,使其成为一个封闭表达式(即,一个表达式,其中定义了每个符号并具有一个值,因此可以对其进行计算)。

例如,当您有一个javascript函数时:

1
2
3
function closed(x) {
  return x + 3;
}

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

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

1
2
3
function open(x) {
  return x*y + 3;
}

它是一个开放式表达式,因为其中有尚未定义的符号。也就是说,y。在查看这个函数时,我们不能分辨y是什么,它是什么意思,我们不知道它的值,所以我们不能计算这个表达式。也就是说,在我们告诉y在其中的含义之前,我们不能调用这个函数。这个y被称为自由变量。

这个y请求一个定义,但这个定义不是函数的一部分,而是在它的"周围环境"(也称为环境)中的其他地方定义的。至少我们希望如此:p

例如,它可以全局定义:

1
2
3
4
5
var y = 7;

function open(x) {
  return x*y + 3;
}

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

1
2
3
4
5
6
7
8
9
10
var global = 2;

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

   return function(x) {
     return x*y + 3;
   }

}

环境的一部分,在一个表达式中给了自由变量它们的含义,那就是闭包。之所以这样叫它,是因为它通过为所有自由变量提供这些缺失的定义,将一个打开的表达式转换为一个关闭的表达式,这样我们就可以计算它了。

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

1
2
3
4
5
{
  global: 2,
  w:"unused",
  y: [whatever has been passed to that wrapper function as its parameter `y`]
}

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

1
2
3
{
  y: [whatever has been passed to that wrapper function as its parameter `y`]
}

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

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

值得注意的是,在上面的示例中,包装函数以值的形式返回其内部函数。从定义(或创建)函数的那一刻起,我们调用这个函数的那一刻就可以是远程的。特别是,它的包装函数不再运行,它在调用堆栈上的参数也不再存在:p这是一个问题,因为内部函数在调用时需要y在那里!换句话说,它需要变量从其闭包以某种方式比包装函数长寿,并在需要时存在。因此,内部函数必须对这些变量进行快照,使其关闭,并将它们存储在安全的地方,以备以后使用。(在调用堆栈之外的某个地方。)

这就是为什么人们经常混淆闭包这个词,把它当成可以对所使用的外部变量进行快照的特殊类型的函数,或者把用来存储这些变量供以后使用的数据结构。但我希望您现在理解,它们不是闭包本身——它们只是在编程语言中实现闭包的方法,或者是允许函数闭包中的变量在需要时存在的语言机制。关于闭包有很多误解(不必要的)使这个主题比实际更混乱和复杂。


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

1
2
3
4
5
6
7
8
9
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

为了有助于理解闭包,可以检查如何用过程语言实现闭包。这个解释将遵循方案中闭包的简单实现。

首先,我必须介绍名称空间的概念。在方案解释器中输入命令时,它必须计算表达式中的各种符号并获取它们的值。例子:

1
2
3
4
5
(define x 3)

(define y 4)

(+ x y) returns 7

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

但是,在方案中,有些表达式允许您临时重写符号的值。下面是一个例子:

1
2
3
4
5
6
7
8
(define x 3)

(define y 4)

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

x returns 3

let关键字的作用是引入一个新的名称空间,其中x值为5。你会注意到它仍然可以看到y是4,使得返回的值为9。您还可以看到,一旦表达式结束,x将恢复为3。从这个意义上讲,x被局部值暂时掩盖了。

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

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

现在让我们跳到第一类函数的实现。或多或少,函数是一组指令,当函数在返回值中被调用时执行。当我们读入一个函数时,我们可以在后台存储这些指令,并在调用函数时运行它们。

1
2
3
4
5
6
7
(define x 3)

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

(let ((x 5))
  (plus-x 4)) returns ?

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

然而,Scheme、CommonLisp和许多其他语言都有所谓的词汇范围——除了存储操作(+x y)之外,我们还将命名空间存储在那个特定的点上。这样,当我们查找值时,我们可以看到,在这个上下文中,x实际上是3。这是一个终结。

1
2
3
4
5
6
7
(define x 3)

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

(let ((x 5))
  (plus-x 4)) returns 7

总之,我们可以使用一个链接列表来存储函数定义时命名空间的状态,允许我们从封闭范围访问变量,并提供我们在不影响程序其余部分的情况下本地屏蔽变量的能力。


这里有一个现实世界的例子说明为什么闭包会踢屁股…这完全是我的javascript代码。让我举例说明。

1
2
3
4
5
6
7
8
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);
};

以下是您将如何使用它:

1
2
3
4
var startPlayback = function(track) {
  Player.play(track);  
};
startPlayback(someTrack);

现在假设您希望播放延迟,例如在运行此代码段后5秒。好吧,使用EDOCX1[0]很容易,而且它是关闭的:

1
2
startPlayback.delay(5000, someTrack);
// Keep going, do other things

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

如果没有闭包,您将不得不在函数外部以某种方式维护这些变量的状态,从而将函数外部的代码与逻辑上属于函数内部的内容一起丢弃。使用闭包可以大大提高代码的质量和可读性。


DR

闭包是一个函数,它的作用域被分配(或用作)一个变量。因此,名称闭包:作用域和函数与任何其他实体一样被封闭和使用。

深入的维基百科风格解释

根据维基百科的说法,结束语是:

Techniques for implementing lexically scoped name binding in languages with first-class functions.

那是什么意思?我们来看看一些定义。

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

1
2
3
4
5
6
7
8
9
10
11
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)

第一类函数

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

在上面的示例中,startAt返回一个(匿名)函数,该函数被分配给closure1closure2。因此,正如您看到的,JavaScript像对待任何其他实体一样对待函数(一流的公民)。

名称绑定

名称绑定是为了找出变量(标识符)引用的数据。作用域在这里非常重要,因为它将决定如何解析绑定。

在上面的示例中:

  • 在内部匿名函数的作用域内,y3绑定。
  • startAt的范围内,x15绑定(视关闭情况而定)。

在匿名函数的作用域内,x不受任何值的约束,因此需要在一个上限(startAt的作用域内进行解析。

词法范围

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

Is the region of a computer program where the binding is valid: where the name can be used to refer to the entity.

有两种方法:

  • 词法(静态)作用域:通过搜索变量的包含块或函数来解析变量的定义,如果搜索失败,则搜索外部包含块,依此类推。
  • 动态作用域:先搜索调用函数,然后搜索调用该调用函数的函数,依此类推,继续调用堆栈。

有关更多解释,请查看这个问题并查看维基百科。

在上面的例子中,我们可以看到javascript在词法上的作用域,因为当解析x时,绑定是在上(startAt的作用域中搜索的,基于源代码(查找x的匿名函数是在startAt中定义的),而不是基于调用堆栈,函数的方式(作用域)是caLLED。

包(包)起来

在我们的示例中,当我们调用startAt时,它将返回一个(一级)函数,该函数将被分配给closure1closure2,因此会创建一个闭包,因为传递的变量15将保存在startAt的作用域内,该作用域将被返回的匿名函数包围。当我们用同一个参数(3通过closure1closure2调用这个匿名函数时,会立即找到y的值(因为这是该函数的参数),但x不受匿名函数范围的限制,所以解析继续在(词汇)上函数范围内进行。(这是在闭幕时保存的),其中发现x15绑定。现在我们知道了求和的所有内容,所以结果可以返回,然后打印出来。

现在,您应该了解闭包以及它们的行为,这是JavaScript的基本部分。

咖喱

哦,您还了解了curring的含义:使用函数(闭包)传递操作的每个参数,而不是使用一个带有多个参数的函数。


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

1
2
3
4
5
6
7
8
9
10
11
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
}

SRC:https://leanpub.com/javascriptallongesix/read leanpub auto如果没有自由变量的函数是纯闭包,则为不纯闭包。


在正常情况下,变量受范围规则约束:局部变量仅在定义的函数内工作。关闭是为了方便而暂时打破这一规则的一种方式。

1
2
3
def n_times(a_thing)
  return lambda{|n| a_thing * n}
end

在上面的代码中,lambda(|n| a_thing * n}是闭包,因为a_thing被lambda(匿名函数创建者)引用。

现在,如果将生成的匿名函数放入一个函数变量中。

1
foo = n_times(4)

foo将打破正常范围规则,并开始在内部使用4。

1
foo.call(3)

返回12。


简而言之,函数指针只是指向程序代码库中某个位置的指针(如程序计数器)。而闭包=函数指针+堆栈帧。

.


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

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

内部函数可以访问在其自身范围、外部函数范围和全局范围中定义的变量。外部函数可以访问其自身作用域和全局作用域中定义的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
******************
Example of Closure
******************

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();

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


这是另一个真实的例子,使用了一种在游戏中很流行的脚本语言——Lua。我需要稍微改变库函数的工作方式,以避免stdin不可用的问题。

1
2
3
4
5
6
7
8
9
local old_dofile = dofile

function dofile( filename )
  if filename == nil then
    error( 'Can not use default of stdin.' )
  end

  old_dofile( filename )
end

当这段代码完成它的作用域(因为它是本地的)时,旧的"dofile"的值将消失,但是该值已包含在一个闭包中,因此新的重新定义的dofile函数可以访问它,或者更确切地说,与函数一起存储的副本作为"upvalue"。


当前:它允许您通过只传递函数参数的一个子集来部分地评估函数。考虑一下:

1
2
3
4
5
6
7
8
9
function multiply (x, y) {
  return x * y;
}

const double = multiply.bind(null, 2);

const eight = double(4);

eight == 8;

闭包:闭包只不过是访问函数范围之外的变量。必须记住,函数或嵌套函数内的函数不是闭包。当需要访问函数范围之外的变量时,总是使用闭包。

1
2
3
4
5
6
7
8
9
10
function apple(x){
   function google(y,z) {
    console.log(x*y);
   }
   google(7,2);
}

apple(3);

// the answer here will be 21

如果您来自Java世界,可以将闭包与类的成员函数进行比较。看看这个例子

1
2
3
4
5
6
7
var f=function(){
  var a=7;
  var g=function(){
    return a;
  }
  return g;
}

函数g是一个闭包:g在中关闭a。因此,g可以与成员函数进行比较,a可以与类字段进行比较,f可以与类进行比较。


闭包每当我们在另一个函数中定义了一个函数时,内部函数就可以访问声明的变量。在外部功能中。闭包最好用示例来解释。在清单2-18中,可以看到内部函数从外部范围。外部函数中的变量已由内部函数关闭(或绑定)。因此这个词关闭。这个概念本身很简单,也相当直观。

1
2
3
4
5
6
7
8
9
10
11
Listing 2-18:
    function outerFunction(arg) {
     var variableInOuterFunction = arg;

     function bar() {
             console.log(variableInOuterFunction); // Access a variable from the outer scope
     }
     // Call the local function to demonstrate that it has access to arg
     bar();
    }
    outerFunction('hello closure!'); // logs hello closure!

来源:http://index of.es/varos/basarat%20ali%20syed%20(auth.)-beginning%20node.js apress%20(2014).pdf


请看下面的代码,以更深入地了解闭包:

1
2
3
4
5
        for(var i=0; i< 5; i++){            
            setTimeout(function(){
                console.log(i);
            }, 1000);                        
        }

这里将输出什么?由于关闭,0,1,2,3,4不会是5,5,5,5,5

那么它将如何解决呢?答案如下:

1
2
3
4
5
6
7
       for(var i=0; i< 5; i++){
           (function(j){     //using IIFE          
                setTimeout(function(){
                               console.log(j);
                           },1000);
            })(i);          
        }

让我简单解释一下,当一个函数创建时,直到它在第1个代码中调用了5次循环,但没有立即调用,所以当它调用时,也就是在1秒之后,这是异步的,所以在这个for循环完成之前,将值5存储在var i中,最后执行setTimeout函数5次并打印5,5,5,5,5

这里介绍如何使用IIFE解决问题,即立即调用函数表达式

1
2
3
4
5
       (function(j){  //i is passed here          
            setTimeout(function(){
                           console.log(j);
                       },1000);
        })(i);  //look here it called immediate that is store i=0 for 1st loop, i=1 for 2nd loop, and so on and print 0,1,2,3,4

有关更多信息,请理解执行上下文以了解闭包。

  • 还有一个解决方案可以使用let(ES6功能)解决这个问题,但是在引擎盖下面,上面的功能可以工作。

    1
    2
    3
    4
    5
    6
    7
     for(let i=0; i< 5; i++){          
         setTimeout(function(){
                        console.log(i);
                    },1000);                        
     }

    Output: 0,1,2,3,4

=>更多说明:

在内存中,当for循环执行以下图片制作:

循环1)

1
2
3
     setTimeout(function(){
                    console.log(i);
                },1000);

循环2)

1
2
3
     setTimeout(function(){
                    console.log(i);
                },1000);

循环3)

1
2
3
     setTimeout(function(){
                    console.log(i);
                },1000);

循环4)

1
2
3
     setTimeout(function(){
                    console.log(i);
                },1000);

循环5)

1
2
3
     setTimeout(function(){
                    console.log(i);
                },1000);

这里我不执行,然后在完成循环后,var i将值5存储在内存中,但是它的作用域在它的子函数中总是可见的,所以当函数在setTimeout中执行时,它会打印五次5,5,5,5,5

所以要解决这个问题,请使用上面解释的IIFE。


来自LuA.Org:

When a function is written enclosed in another function, it has full access to local variables from the enclosing function; this feature is called lexical scoping. Although that may sound obvious, it is not. Lexical scoping, plus first-class functions, is a powerful concept in a programming language, but few languages support that concept.


推荐阅读

    Sublime Text3下,实现函数声明的跳转

    Sublime Text3下,实现函数声明的跳转,插件,括号,用了ST3很久,觉得这编辑器特别顺手好用。项目是Cocos2d-x + Lua。可惜的是,ST3不支持函数声

    闭包是什么

    闭包是什么,闭包,函数,引用,变量,语言,运行,闭包是什么?计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),

    Python中的闭包详解

    Python中的闭包详解,函数,闭包,状态,参数,事件,变量,Python中的闭包是一种高级特性,它可以让我们更加灵活地使用函数。在这篇文章中,我们将详细介

    函数本质、闭包函数

    函数本质、闭包函数,函数,名字,python三大神器,装饰器,迭代器,生成器time模块------时间模块print(time.time())时间戳----------->这个时间

    关于lua的闭包(Closure)和Upvalue

    关于lua的闭包(Closure)和Upvalue,函数,变量, 关于lua的闭包(Closure)和Upvalue upvalue:嵌套函数的外部函数的局部变量 function func(a) <=

    啫喱app详细介绍(咖喱APP)

    啫喱app详细介绍(咖喱APP),最近有朋友听说了一款名为啫喱的软件上线,但是不知道啫喱app是什么,其实这就是一款借用元宇宙概念的社交软件。

    版权声明怎么写(版权声明怎么写?)

    版权声明怎么写(版权声明怎么写?),中国,日本,题目,伪满,目的,概括性,  她的那方面版权声明怎么写,,,还是全面的,她是日本人一手制造的伪中国

    免责声明

    免责声明,平台,版权,机构,下载,链接,网站,  1、我方平台为信息发布平台,您的留言将在我方平台发布或提供给相应服务商。2、如不需要发布