我问了一个关于咖喱和闭包的问题。什么是关闭?它与咖喱有什么关系?
可变范围
当您声明一个局部变量时,该变量有一个作用域。通常,局部变量只存在于声明它们的块或函数中。
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已关闭—它在关闭范围内。
注意变量a对fnc是完全私有的。这是一种在函数式编程语言(如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返回一个(匿名)函数,该函数被分配给closure1和closure2。因此,正如您看到的,JavaScript像对待任何其他实体一样对待函数(一流的公民)。
名称绑定
名称绑定是为了找出变量(标识符)引用的数据。作用域在这里非常重要,因为它将决定如何解析绑定。
在上面的示例中:
- 在内部匿名函数的作用域内,y与3绑定。
- 在startAt的范围内,x与1或5绑定(视关闭情况而定)。
在匿名函数的作用域内,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时,它将返回一个(一级)函数,该函数将被分配给closure1和closure2,因此会创建一个闭包,因为传递的变量1和5将保存在startAt的作用域内,该作用域将被返回的匿名函数包围。当我们用同一个参数(3通过closure1和closure2调用这个匿名函数时,会立即找到y的值(因为这是该函数的参数),但x不受匿名函数范围的限制,所以解析继续在(词汇)上函数范围内进行。(这是在闭幕时保存的),其中发现x与1或5绑定。现在我们知道了求和的所有内容,所以结果可以返回,然后打印出来。
现在,您应该了解闭包以及它们的行为,这是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(匿名函数创建者)引用。
现在,如果将生成的匿名函数放入一个函数变量中。
foo将打破正常范围规则,并开始在内部使用4。
返回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 |
有关更多信息,请理解执行上下文以了解闭包。
=>更多说明:
在内存中,当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.