我问了一个关于柯里化的问题,提到了闭包。什么是闭包?它与柯里化有什么关系?
我问了一个关于柯里化的问题,提到了闭包。什么是闭包?它与柯里化有什么关系?
当你声明一个局部变量时,该变量就有了作用域。通常情况下,局部变量只在你声明它们的块或函数中存在。
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
退出后仍然存在,那么“内部”函数定义时所在的作用域中的所有变量也会一直存在。 变量a
已被封闭 - 它在一个闭包内。
请注意,变量a
对于fnc
来说是完全私有的。 这是在诸如JavaScript之类的函数式编程语言中创建私有变量的一种方式。
正如您可能猜到的那样,当我调用fnc()
时,它会打印变量a
的值,该值为“1”。
在没有闭包的语言中,当函数outer
退出时,变量a
将被垃圾回收并丢弃。调用fnc
将引发错误,因为变量a
已不存在。
在JavaScript中,变量a
会持续存在,因为变量作用域是在函数首次声明时创建的,并且会在函数继续存在的同时一直存在。
a
属于outer
的作用域。 inner
的作用域具有指向outer
作用域的父指针。 fnc
是指向inner
的变量。只要fnc
存在,a
就会一直存在。变量a
位于闭包内。
我制作了一个 YouTube 视频,其中包含此代码的一些实际示例。
我会举一个例子(用JavaScript语言):
function makeCounter () {
var count = 0;
return function () {
count += 1;
return count;
}
}
var x = makeCounter();
x(); returns 1
x(); returns 2
...etc...
makeCounter
函数返回一个函数 x
,每次调用它都会递增一。由于我们没有向 x
提供任何参数,所以它必须以某种方式记住计数。它根据词法作用域找到值的位置 - 它必须查找定义它的位置来找到该值。这个“隐藏”的值就是所谓的闭包。
这里是我的柯里化示例:
function add (a) {
return function (b) {
return a + b;
}
}
var add3 = add(3);
add3(4); returns 7
你可以看到,当你调用参数为a的add函数(值为3)时,该值包含在我们定义的add3返回的闭包中。这样,当我们调用add3时,它知道在哪里找到a值来执行加法。add3
赋的值是一个已经计算过的函数调用,也就是说它的值 是 一个带有一个参数 b
的函数。当你调用 add3(4)
时,你正在调用该函数,该函数“记住”变量 a
及其值 3
,即使 a
在其函数块之外并且在某种意义上“不再存在”。这是因为在声明 add3
时,变量 a
在内部函数的词法作用域内是可访问的。在其词法作用域内,内部引用和外部变量之间的连接是持久的。 - user1063287function 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://dev59.com/1nVC5IYBdhLWcg3wqzDV#36878651
值得注意的是,在上面的例子中,包装函数将其内部函数作为值返回。我们调用此函数的时刻可能与定义(或创建)函数的时刻相距很远。特别是,封装函数不再运行,调用栈上的参数也不再存在 :P 这造成了问题,因为内部函数需要在调用时有y!换句话说,它需要从其闭包中获取变量以某种方式存活于包装函数之外,并在需要时出现。因此,内部函数必须对构成其闭包的这些变量进行快照,并将它们安全地存储在其他地方供以后使用。(在调用栈之外的某个地方。)
这就是为什么人们经常将术语闭包误解为那种特殊类型的函数,它可以拍摄它们使用的外部变量的快照,或用于存储这些变量以便以后使用的数据结构。但是我希望您现在理解它们不是闭包本身——它们只是实现闭包的编程语言中的一种方式,或允许函数闭包中的变量在需要时存在的语言机制。关于闭包存在很多误解,这使得这个主题比它实际上更加混乱和复杂。
凯尔的回答相当不错。我认为唯一需要补充的是,闭包实际上是在创建 lambda 函数时对堆栈的快照。然后,当函数重新执行时,堆栈将恢复到执行该函数之前的状态。因此,正如凯尔所提到的那样,lambda 函数在执行时可以访问隐藏值 (count
)。
闭包是一个函数,它可以引用另一个函数中的状态。例如,在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
为了更好地理解闭包,可以考虑在过程式语言中如何实现闭包。本解释将介绍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 被局部值暂时掩盖了。
过程式语言和面向对象语言有类似的概念。每当您在函数中声明与全局变量同名的变量时,您就会获得相同的效果。
我们该如何实现这个?一种简单的方法是使用链表 - 头包含新值,尾部包含旧命名空间。当您需要查找符号时,您从头开始,沿着尾部往下查找。
现在让我们暂时跳过对一等函数(first-class functions)实现的讨论。更或少地说,函数是一组指令,在调用函数时执行并产生返回值。当我们读取一个函数时,我们可以将这些指令存储在幕后,并在调用函数时运行它们。
(define x 3)
(define (plus-x y)
(+ x y))
(let ((x 5))
(plus-x 4)) returns ?
我们将x定义为3,plus-x为其参数y加上x的值。最后,在一个屏蔽了x的新值为5的环境中调用plus-x。如果我们仅仅存储操作(+ x y)作为函数plus-x,并且在x的上下文中值为5,则返回的结果将是9。这就是所谓的动态作用域。(define x 3)
(define (plus-x y)
(+ x y))
(let ((x 5))
(plus-x 4)) returns 7
总之,我们可以使用链表来存储函数定义时名称空间的状态,这样就可以访问封闭作用域中的变量,并且提供了在不影响程序其余部分的情况下局部屏蔽变量的能力。
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
}
以下是一个实际案例,说明为什么闭包很牛逼... 这是我 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
回调发生时,闭包仍然保持这些变量,因此它可以使用原始参数调用原始函数。闭包是一个函数及其作用域被分配给(或用作)变量。因此,它的名称为闭包:作用域和函数被封装并像任何其他实体一样使用。
在具有头等函数的语言中实现词法作用域名称绑定的技术。
这是什么意思?让我们看一些定义。
我将使用这个例子来解释闭包和其他相关定义:
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
的)作用域中解析。
如维基百科所说,作用域:
是计算机程序的区域,在该区域绑定是有效的: 可以使用名称来引用实体。
有两种技术:
在上面的例子中,我们可以看到JavaScript是按词法作用域的,因为当x
被解析时,绑定被搜索在上层(startAt
的)作用域中,基于源代码(匿名函数寻找x是在startAt
内部定义的),而不是基于调用栈,即函数被调用的作用域。
startAt
时,它将返回一个(一等)函数,该函数将被分配给closure1
和closure2
,因此创建了一个闭包,因为传递的变量1
和5
将保存在startAt
的范围内,并将被封装在返回的匿名函数中。当我们通过closure1
和closure2
使用相同的参数(3
)调用此匿名函数时,y
的值将立即被找到(因为那是该函数的参数),但是x
没有在匿名函数的作用域中绑定,因此解析将在(词法上)更高的函数作用域(保存在闭包中)中继续进行,在那里x
被发现绑定到1
或5
。现在我们知道所有求和所需的信息,因此可以返回结果,然后打印。
现在您应该理解闭包及其行为,这是JavaScript的基本部分。
哦,而且你也学到了什么是柯里化:你使用函数(闭包)来传递每个操作的参数,而不是使用具有多个参数的一个函数。
闭包是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,即它的内部函数自身变量、外部函数变量和全局变量值的总和。