'闭包是什么?

我问了一个关于curry和闭包的问题。 什么是闭包?它和咖喱有什么关系?< / p >
174280 次浏览

我将给出一个例子(JavaScript):

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提供任何参数,它必须以某种方式记住计数。它知道根据所谓的词法作用域在哪里找到它——它必须查找定义值的位置才能找到值。这个“hidden"值就是所谓的闭包。

下面还是我用咖喱的例子:

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


var add3 = add(3);
    

add3(4); returns 7

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

闭包是一个可以引用另一个函数中的状态的函数。例如,在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

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

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

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

现在,如果你把得到的匿名函数放到一个函数变量中。

foo = n_times(4)

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

foo.call(3)

返回12。

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

为了帮助理解闭包,研究一下如何在过程语言中实现闭包可能会很有用。本解释将遵循Scheme中闭包的简单实现。

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

(define x 3)


(define y 4)


(+ x y) returns 7

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

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

(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被局部值暂时掩盖了。

过程式语言和面向对象语言都有类似的概念。无论何时在函数中声明一个与全局变量同名的变量,都会得到相同的效果。

我们如何实现它呢?一种简单的方法是使用链表——头部包含新值,尾部包含旧的名称空间。当你需要查找一个符号时,你从头部开始,一直到尾部。

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

(define x 3)


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


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

我们定义x为3,加-x为它的参数y,加上x的值。最后,在x被一个新的x遮蔽的环境中,我们调用加-x,这个x的值为5。如果我们只存储函数+ x的(+ xy)操作,因为我们在x = 5的情况下,返回的结果将是9。这就是所谓的动态作用域。

然而,Scheme、Common Lisp和许多其他语言都有所谓的词法作用域——除了存储操作(+ x y)之外,我们还将名称空间存储在特定的位置。这样,当我们查找值的时候,我们可以看到x,在这里,实际上是3。这是一个闭包。

(define x 3)


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


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

总之,我们可以使用链表来存储函数定义时名称空间的状态,从而允许我们从封闭的作用域访问变量,并且能够在不影响程序其余部分的情况下对变量进行局部屏蔽。

这里有一个现实世界的例子,为什么闭包这么厉害…这是我的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);
};

下面是你如何使用它:

var startPlayback = function(track) {
Player.play(track);
};
startPlayback(someTrack);

现在,假设您希望回放延迟开始,例如在此代码段运行后5秒。好吧,这很容易用delay和它的闭包:

startPlayback.delay(5000, someTrack);
// Keep going, do other things
当你用5000ms调用delay时,第一个代码段运行,并将传入的参数存储在它的闭包中。5秒后,当setTimeout回调发生时,闭包仍然保持这些变量,因此它可以使用原始参数调用原始函数 这是一种咖喱,或功能装饰

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

下面是另一个现实生活中的例子,使用了游戏中流行的脚本语言——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

当这段代码完成它的作用域时,old_dofile的值就消失了(因为它是本地的),但是该值已经被封装在一个闭包中,所以新的重新定义的dofile函数可以访问它,或者更确切地说,作为“upvalue”与函数一起存储的副本。

变量作用域

声明局部变量时,该变量具有作用域。一般来说,局部变量只存在于声明它们的块或函数中。

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

如果我试图访问一个局部变量,大多数语言都会在当前作用域中查找它,然后向上遍历父作用域,直到到达根作用域。

var a = 1;
function() {
console.log(a); // works
}
console.log(a); // works

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

这就是我们通常期望的事情的运作方式。

闭包是一个持久的局部变量作用域

闭包是一个持久作用域,即使在代码执行已经移出该块之后,它仍然保留局部变量。支持闭包的语言(如JavaScript、Swift和Ruby)将允许您保留对作用域(包括其父作用域)的引用,即使在声明这些变量的块已经完成执行之后,只要您在某处保留对该块或函数的引用。

作用域对象及其所有局部变量都绑定到函数,只要函数存在,它们就会存在。

这给了我们函数可移植性。我们可以预期,在函数第一次定义时在作用域中的任何变量,在稍后调用该函数时仍然在作用域中,即使我们在完全不同的上下文中调用该函数。

例如

下面是一个非常简单的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();

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

正常情况下,当一个函数退出时,它的所有局部变量都会消失。然而,如果我们返回内部函数,并将其赋值给变量fnc,以便在outer退出后它仍然存在,则定义inner时范围内的所有变量也会保留. c。变量a已经被关闭——它在一个闭包中。

注意变量a对于fnc是完全私有的。这是在函数式编程语言(如JavaScript)中创建私有变量的一种方式。

你可能已经猜到了,当我调用fnc()时,它会输出a的值,即"1"。

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

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

a属于outer的范围。inner的作用域有一个指向outer作用域的父指针。fnc是一个指向inner的变量。只要fnc存在,a就会存在。a在闭包中。

进一步阅读(观看)

我做了一个YouTube视频查看这段代码的一些实际使用的例子。

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

Lua.org:

当一个函数被封装在另一个函数中编写时,它可以完全访问封装函数中的局部变量;这个特性称为词法作用域。尽管这听起来很明显,但事实并非如此。词法作用域加上第一类函数是编程语言中一个强大的概念,但很少有语言支持这个概念。

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

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

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-if-functions-without-free-variables-are-pure-are-closures-impure

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

var f=function(){
var a=7;
var g=function(){
return a;
}
return g;
}

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

首先,与这里大多数人告诉你的相反,闭包是not一个函数!那么是什么呢?
它是在函数的“周围上下文”中定义的符号的;(称为环境),使其成为CLOSED表达式(即,其中每个符号都有定义并具有值的表达式,因此可以对其求值)

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

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

它是封闭的表达式,因为其中出现的所有符号都在其中定义(它们的含义是明确的),所以你可以对它求值。换句话说,它是自包含的

但如果你有一个这样的函数

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

它是开放的表达,因为其中有一些符号没有在其中定义。也就是说,y。在查看这个函数时,我们不能说出y是什么以及它的含义,我们不知道它的值,所以我们不能对这个表达式求值。也就是说,我们不能调用这个函数,直到我们告诉y在它里面应该是什么意思。这个y被称为自由变量

y请求定义,但这个定义不是函数的一部分——它是在其他地方定义的,在它的“周围上下文”中;(也称为环境)。至少这是我们所希望的:P

例如,它可以被全局定义:

var y = 7;


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

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

var global = 2;


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


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

环境中赋予表达式中自由变量含义的部分是关闭。之所以这样称呼它,是因为它通过为其所有自由变量提供这些缺失的定义,将开放表达式转换为关闭表达式,以便我们可以对其求值。

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

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

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

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

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

更多背后的理论: https://stackoverflow.com/a/36878651/434562 < / p >

值得注意的是,在上面的示例中,包装器函数将其内部函数作为值返回。我们调用这个函数的时间可以从函数定义(或创建)的时刻开始。特别是,它的包装函数不再运行,它在调用堆栈上的参数也不再存在:P这就产生了一个问题,因为内部函数在被调用时需要y在那里!换句话说,它要求闭包中的变量以某种方式包装器函数,并在需要时出现。因此,内部函数必须为这些变量创建快照,以使其闭包并将它们存储在安全的地方以供以后使用。(在调用堆栈之外的某个地方。)

这就是为什么人们经常混淆术语关闭是一种特殊类型的函数,它可以对他们使用的外部变量或用于存储这些变量的数据结构进行快照。但我希望你现在明白它们是闭包本身——它们只是编程语言中实现闭包的方法,或允许函数闭包中的变量在需要时出现的语言机制。关于闭包有很多误解,这些误解(不必要地)使这个主题比实际情况更令人困惑和复杂。

博士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)

一级函数

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

在上面的例子中,startAt返回一个(匿名)函数,该函数被赋值给closure1closure2。如你所见,JavaScript对待函数就像对待其他实体一样(一级公民)。

名绑定

名绑定是关于找出什么数据是变量(标识符)参考文献。作用域在这里非常重要,因为它将决定如何解析绑定。

在上面的例子中:

  • 在内部匿名函数的作用域中,y绑定到3
  • startAt的作用域中,x被绑定到15(取决于闭包)。

在匿名函数的作用域内,x没有绑定到任何值,因此需要在upper (startAt’s)作用域中解析它。

词法作用域

作为维基百科上说,作用域:

是计算机程序中绑定有效的区域:在哪里可以使用名称来指代实体

有两种方法:

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

更多解释,看看这个问题看看维基百科

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

裹(合)起来

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

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

局部套用

哦,你还了解了局部套用是关于什么:你使用函数(闭包)来传递操作的每个参数,而不是使用一个带有多个形参的函数。

< p >关闭 只要在另一个函数内部定义了一个函数,内部函数就可以访问声明的变量 在外层函数中。闭包最好用例子来解释。 在清单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)。他们% 20 node.js-apress % 20 (2014) . pdf

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

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

让我简单解释一下,当一个函数创建时,直到它在第一个代码中调用了5次for循环,但没有立即调用,所以当它在1秒后调用i.e时,而且这是异步的,所以在这个for循环结束之前,将值5存储在var i中,最后执行setTimeout函数5次并打印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);
    }
    
    
    Output: 0,1,2,3,4
    

=> More explanation:

In memory, when for loop execute picture make like below:

Loop 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没有被执行,然后在完成循环后,var i在内存中存储值5,但它的作用域在它的子函数中始终可见,因此当函数在setTimeout中执行五次时,它打印5,5,5,5,5

所以解决这个问题使用IIFE如上所述。

关闭是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();

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

curry:它允许你通过只传入函数参数的子集来部分求值。考虑一下:

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
关闭非常简单。我们可以这样考虑: 闭包=函数+它的词法环境

考虑以下函数:

function init() {
var name = “Mozilla”;
}

在上面的情况下闭包是什么? 函数init()及其词法环境中的变量,即name。 闭包 = init() + name

考虑另一个函数:

function init() {
var name = “Mozilla”;
function displayName(){
alert(name);
}
displayName();
}

这里的闭包是什么? 内部函数可以访问外部函数的变量。displayName()可以访问父函数init()中声明的变量名。但是,如果displayName()中的局部变量存在,则将使用相同的局部变量

终结1: init函数+ (name变量+ displayName()函数)——>词法作用域

终结2: displayName函数+(名称变量)——>词法范围

闭包为JavaScript提供状态。

状态在编程中仅仅意味着记忆。

例子

var a = 0;


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

在上面的例子中,state存储在变量“a”中。我们在a后面加几次1。我们之所以能够这样做,是因为我们能够“记住”这个值。状态符“a”将该值保存在内存中。

通常,在编程语言中,您希望跟踪事物、记住信息并在以后访问它。

其他语言通常是通过使用类来实现的。类,就像变量一样,跟踪它的状态。而该类的实例,依次在它们内部也有状态。状态仅仅意味着您以后可以存储和检索的信息。

例子

class Bread {
constructor (weight) {
this.weight = weight;
}


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

我们如何从“渲染”方法中访问“权重”?好吧,感谢国家。Bread类的每个实例都可以通过从“状态”(内存中存储信息的地方)读取它来呈现自己的权重。

现在,JavaScript是一种非常独特的语言在历史上没有类(现在有了,但在底层只有函数和变量),所以闭包为JavaScript提供了一种记忆东西并在以后访问它们的方法。

例子

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


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

上面的例子实现了用变量“保持状态”的目标。这太棒了!然而,这有一个缺点,即变量(“状态”holder)现在是公开的。我们可以做得更好。我们可以使用闭包。

例子

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”。在任何情况下,shape或form任何人都无法访问“n”,除非通过API

JavaScript的简单性确实令人惊叹。

闭包是其中一个重要原因。

在Groovy中有一个简单的例子供您参考:

def outer() {
def x = 1
return { -> println(x)} // inner
}
def innerObj = outer()
innerObj() // prints 1

下面的示例演示了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