“闭包”是什么?

537

我问了一个关于柯里化的问题,提到了闭包。什么是闭包?它与柯里化有什么关系?


33
闭包是什么?有些回答说,闭包是函数;有些说它是堆栈;还有些回答称其为“隐藏”的值。据我理解,它是由函数和封闭变量组成的。 - Roland
5
闭包是一种函数,它可以捕获并存储其被创建时所在环境的状态信息,并在其词法范围之外被调用。 闭包通常用于实现数据隐藏和封装,以及在JavaScript中创建模块或命名空间。 它们是JavaScript中强大且常见的编程概念之一。 关于为什么称其为“闭包”,这可能是因为它可以“封闭”或包含其被创建时的环境。 - dietbuddha
2
请参考软件工程领域 stackexchange 上的 什么是闭包? - Felix K.
这里有很棒的答案。然而,如果你对数学有兴趣,值得查看 https://dev59.com/1nVC5IYBdhLWcg3wqzDV#36878651。 - KGhatak
22个回答

4
在正常情况下,变量受到作用域规则的限制:局部变量仅在定义的函数内起作用。闭包是一种为了方便暂时打破这个规则的方法。
def n_times(a_thing)
  return lambda{|n| a_thing * n}
end

在上述代码中,lambda(|n| a_thing * n}是闭包,因为lambda(一个匿名函数创建者)引用了a_thing
现在,如果你将结果为匿名函数放入一个函数变量中。
foo = n_times(4)

foo将打破正常的作用域规则并开始在内部使用4。

foo.call(3)

返回 12。


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

2

闭包为JavaScript提供状态。

在编程中,状态简单地指记住事情。

示例

var a = 0;

a = a + 1; // => 1
a = a + 1; // => 2
a = a + 1; // => 3

在上面的案例中,状态存储在变量“a”中。接着我们多次给“a”加1。这是因为我们能够“记住”这个值,只有通过状态持有者“a”,才能将其保存在内存中。
通常,在编程语言中,你希望跟踪事物,记住信息并在以后访问它。
在其他编程语言中,通常通过使用类来实现。类就像变量一样跟踪它的状态。而该类的实例则也在其中拥有状态。状态简单地指可以存储和检索的信息。
例如:
class Bread {
  constructor (weight) {
    this.weight = weight;
  }

  render () {
    return `My weight is ${this.weight}!`;
  }
}

我们如何在“render”方法内部访问“weight”?感谢状态。Bread类的每个实例都可以通过从“state”中读取它来渲染自己的重量,这是一个我们可以存储信息的内存空间。
现在,JavaScript是一种非常独特的语言,历史上没有类(现在有了,但底层仍然只有函数和变量),因此闭包提供了一种方法,使JavaScript能够记住事物并在以后访问它们。
举个例子:
var n = 0;
var count = function () {
  n = n + 1;
  return n;
};

count(); // # 1
count(); // # 2
count(); // # 3

上面的示例通过变量实现了“保持状态”的目标。这很好!然而,这种方法的缺点是变量(即“状态”持有者)现在被暴露出来了。我们可以做得更好。我们可以使用闭包。

示例

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

  return count;
};

var count = countGenerator();
count(); // # 1
count(); // # 2
count(); // # 3

这太棒了。

现在我们的“count”函数可以计数了。它之所以能够这样做,是因为它可以“保持”状态。在这种情况下,状态是变量“n”。这个变量现在被关闭了。在时间和空间上都是如此。在时间上,因为你永远不能恢复它、改变它、赋值或直接与它交互。在空间上,因为它地理上嵌套在“countGenerator”函数中。

为什么这很棒?因为我们不需要涉及任何其他复杂的工具(例如类、方法、实例等),就能够 1. 隐藏 2. 远程控制

我们隐藏了状态,变量“n”,使其成为私有变量! 我们还创建了一个API,可以按预定义的方式控制这个变量。特别地,我们可以像这样调用API“count()”,从“远程”将1添加到“n”。任何人都无法通过任何方式访问“n”,除非通过API。

JavaScript在其简单性方面真是太神奇了。

这部分原因在于闭包。


0
如果你来自Java世界,你可以将闭包与类的成员函数进行比较。看看这个例子。
var f=function(){
  var a=7;
  var g=function(){
    return a;
  }
  return g;
}

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

0
这是一个示例,演示了Scheme编程语言中的闭包。
首先,我们定义一个函数来定义一个本地变量,在函数外部不可见。
; Function using a local variable
(define (function)
  (define a 1)
  (display a) ; prints 1, when calling (function)
  )
(function) ; prints 1
(display a) ; fails: a undefined

这是同样的例子,但现在该函数使用一个在函数外定义的全局变量。

; Function using a global variable
(define b 2)
(define (function)
  (display b) ; prints 2, when calling (function)
  )
(function) ; prints 2
(display 2) ; prints 2

最后,这里有一个函数携带自己闭包的例子:
; Function with closure
(define (outer)
  (define c 3)
  (define (inner)
    (display c))
  inner ; outer function returns the inner function as result
  )
(define function (outer))
(function) ; prints 3

0

柯里化:它允许您仅通过传递其子集来部分评估函数。考虑以下内容:

function multiply (x, y) {
  return x * y;
}

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

const eight = double(4);

eight == 8;

闭包:闭包不过是访问函数作用域外的变量。需要记住的是,一个函数内部或嵌套函数并不是闭包。只有在需要访问函数作用域外的变量时才会使用闭包。
function apple(x){
   function google(y,z) {
    console.log(x*y);
   }
   google(7,2);
}

apple(3);

// the answer here will be 21

0
这是另一个真实案例,使用在游戏中流行的脚本语言Lua。我需要稍微改变库函数的工作方式,以避免stdin不可用的问题。
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”存储在函数中的副本。

0

来自Lua.org

当一个函数被写在另一个函数中时,它可以完全访问封闭函数的本地变量;这个特性被称为词法作用域。虽然这听起来很明显,但实际上并不是。词法作用域加上一级函数是编程语言中的一个强大概念,但很少有语言支持这个概念。


0
请看下面的代码,更深入地了解闭包:
        for(var i=0; i< 5; i++){            
            setTimeout(function(){
                console.log(i);
            }, 1000);                        
        }

以下是代码输出结果:

0, 1, 2, 3, 4

不会是 5,5,5,5,5,因为闭包的原因。

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

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

让我简单解释一下,当一个函数被创建时,除非被调用否则不会发生任何事情。所以第一个代码中的for循环调用了5次,但并没有立即调用, 因此在1秒之后才会调用。由于是异步的,因此在这个for循环完成并将值5存储在变量i中之前,最终执行setTimeout函数五次,并打印"5,5,5,5,5"

以下是如何使用IIFE(立即调用函数表达式)来解决这个问题

       (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特性)解决这个问题的解决方案,但在上面的函数底层工作。
 for(let i=0; i< 5; i++){           
     setTimeout(function(){
                    console.log(i);
                },1000);                        
 }

输出: 0,1,2,3,4

=> 更多解释:

在内存中,当 for 循环执行时,图片会像下面这样:

循环 1)

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

循环2)

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

循环3)

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

循环 4)

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

循环 5)

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

这里的 i 没有被执行,然后在循环完成后,变量 i 存储了值 5 在内存中,但它的作用域始终可见于其子函数,因此当函数在 setTimeout 中执行五次时,它会打印出 5,5,5,5,5

因此,为了解决这个问题,请按照上面所述使用 IIFE。


谢谢你的回答。如果您将代码与解释分离,它会更易读。(不要缩进非代码行) - eMBee

0
闭包 每当我们在一个函数内定义另一个函数时,内部函数就可以访问在外部函数中被声明的变量。闭包最好通过例子来解释。 在列表2-18中,您可以看到内部函数可以访问来自外部作用域的变量(variableInOuterFunction)。外部函数中的变量已被内部函数封闭(或绑定)。因此术语闭包。这个概念本身足够简单,并且相当直观。
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/Varios/Basarat%20Ali%20Syed%20(auth.)-Beginning%20Node.js-Apress%20(2014).pdf


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接