-
09-06-2019 - |
题
我问了一个关于柯里化的问题,其中提到了闭包。什么是闭包?它与柯里化有什么关系?
解决方案
变量范围
当您声明局部变量时,该变量就有一个作用域。通常,局部变量仅存在于声明它们的块或函数中。
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
被定义也持续存在. 。变量 a
已被关闭——它处于关闭状态。
请注意,变量 a
完全私有 fnc
. 。这是一种在函数式编程语言(例如 JavaScript)中创建私有变量的方法。
正如你可能猜到的,当我打电话时 fnc()
它打印的值 a
, ,即“1”。
在没有闭包的语言中,变量 a
当函数被垃圾收集并丢弃时 outer
退出。调用 fnc 会抛出错误,因为 a
不复存在。
在 JavaScript 中,变量 a
之所以持续存在,是因为变量作用域是在首次声明函数时创建的,并且只要函数继续存在,变量作用域就会持续存在。
a
属于范围 outer
. 。范围 inner
有一个指向范围的父指针 outer
. fnc
是一个变量,它指向 inner
. a
只要持续存在 fnc
持续存在。 a
是在闭包之内。
其他提示
我举一个例子(用 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(即 3)调用 add 时,该值包含在我们定义为 add3 的返回函数的闭包中。这样,当我们调用 add3 时,它知道在哪里找到 a 值来执行加法。
凯尔的回答 还不错。我认为唯一需要说明的是,闭包基本上是创建 lambda 函数时堆栈的快照。然后,当函数重新执行时,堆栈将恢复到执行函数之前的状态。因此,正如凯尔提到的,隐藏的价值(count
) 在 lambda 函数执行时可用。
首先,与这里大多数人告诉你的相反, 闭包是 不是 一个函数!所以呢 是 它?
它是一个 放 在函数的“周围上下文”(称为其 环境),这使其成为 CLOSED 表达式(即,每个符号都已定义并具有值的表达式,因此可以对其进行求值)。
例如,当您有一个 JavaScript 函数时:
function closed(x) {
return x + 3;
}
它是一个 封闭式表达 因为其中出现的所有符号都在其中定义了(它们的含义很明确),所以你可以评估它。换句话说,就是 独立的.
但如果你有这样的函数:
function open(x) {
return x*y + 3;
}
它是一个 开放式表达 因为里面有一些符号没有被定义。即, y
. 。当我们看这个函数时,我们无法分辨出什么 y
是,它是什么意思,我们不知道它的值,所以我们无法计算这个表达式。IE。我们不能调用这个函数,直到我们知道什么 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 这会产生问题,因为内部函数需要 y
当它被召唤时就在那里!换句话说,它需要从闭包中获取变量以某种方式 比寿命长 包装函数并在需要时出现。因此,内部函数必须做出一个 快照 这些变量使其关闭并将它们存储在安全的地方以供以后使用。(调用堆栈之外的某个地方。)
这就是为什么人们经常混淆这个词 关闭 是一种特殊类型的函数,可以对它们使用的外部变量或用于存储这些变量的数据结构进行快照以供以后使用。但我希望你现在明白他们是 不是 关闭本身——它们只是方法 实施 编程语言中的闭包,或者允许函数闭包中的变量在需要时存在的语言机制。关于闭包存在很多误解,这些误解(不必要地)使这个主题比实际情况更加混乱和复杂。
闭包是一个可以引用另一个函数中的状态的函数。例如,在 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
定义表达式将值 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,plus-x 为其参数 y,加上 x 的值。最后,我们在 x 被新 x 屏蔽的环境中调用 plus-x,该 x 的值为 5。如果我们仅存储函数 plus-x 的操作 (+ x y),因为我们处于 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
你打电话时 delay
和 5000
ms,第一个片段运行,并将传入的参数存储在其闭包中。然后 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 的基本部分。
柯里化
哦,你还学到了什么 柯里化 是关于:您使用函数(闭包)来传递操作的每个参数,而不是使用具有多个参数的一个函数。
不包含自由变量的函数称为纯函数。
包含一个或多个自由变量的函数称为闭包。
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
}
在正常情况下,变量受范围规则约束:局部变量仅在定义的函数内起作用。关闭是为了方便而暂时打破此规则的一种方式。
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。
简而言之,函数指针只是指向程序代码库中某个位置的指针(如程序计数器)。然而 闭包=函数指针+栈帧.
.
关闭 是 JavaScript 中的一项功能,其中函数可以访问其自己的作用域变量、外部函数变量以及全局变量。
即使外部函数返回后,闭包也可以访问其外部函数作用域。这意味着即使在函数完成之后,闭包也可以记住并访问其外部函数的变量和参数。
内部函数可以访问其自身作用域、外部函数作用域和全局作用域中定义的变量。并且外部函数可以访问自己作用域和全局作用域中定义的变量。
******************
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。我需要稍微改变库函数的工作方式,以避免标准输入不可用的问题。
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 函数可以访问它,或者更确切地说,与该函数一起存储的副本作为“升值”。
从 Lua.org:
当一个函数被编写为包含在另一个函数中时,它可以完全访问封闭函数中的局部变量;此功能称为词法作用域。尽管这听起来很明显,但事实并非如此。词法作用域加上一等函数是编程语言中的一个强大概念,但很少有语言支持该概念。
如果您来自 Java 世界,您可以将闭包与类的成员函数进行比较。看这个例子
var f=function(){
var a=7;
var g=function(){
return a;
}
return g;
}
功能 g
是一个闭包: g
关闭 a
在。所以 g
可以与成员函数进行比较, a
可以与类字段进行比较,并且函数 f
和一个班级。
每当我们在另一个函数内定义一个函数时,内部函数都会访问外部函数中声明的变量。闭包最好用例子来解释。在列表2-18时,您可以看到内部函数可以访问外部范围的变量(变量函数)。外部函数中的变量已被内部函数关闭(或绑定在内部函数中)。因此,术语闭合。这个概念本身非常简单且相当直观。
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
请看下面的代码以更深入地理解闭包:
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 之前在 var 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); } Output: 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 没有被执行,然后在完成循环之后, var i 在内存中存储了值 5,但它的作用域在它的子函数中始终可见,所以当函数在内部执行时 setTimeout
打印五次 5,5,5,5,5
因此,要解决此问题,请使用 IIFE,如上所述。
柯里化:它允许您通过仅传入函数参数的子集来部分评估函数。考虑一下:
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() 及其词法环境中的变量(即名称)。关闭 = 初始化() + 名称
考虑另一个函数:
function init() {
var name = “Mozilla”;
function displayName(){
alert(name);
}
displayName();
}
这里将关闭什么?内部函数可以访问外部函数的变量。displayName() 可以访问父函数 init() 中声明的变量名称。但是,如果存在,将使用 displayName() 中相同的局部变量。
关闭1: init 函数 + ( 名称变量 + displayName() 函数) --> 词法作用域
关闭2: displayName 函数 + ( 名称变量 ) --> 词法作用域