问题描述
我知道闭包被定义为:
[A]堆栈帧,当函数返回时不会释放。 (好像'stack-frame'是malloc'ed而不是堆栈!)
但我不明白这个答案如何适应JavaScript的存储机制。 口译员如何跟踪这些价值观? 浏览器的存储机制是否以类似于堆和堆栈的方式进行分段?
关于这个问题的答案: 说明:
[A]函数引用也有一个关闭的秘密引用
这个神秘的“秘密参考”背后的潜在机制是什么?
编辑许多人说这是依赖于实现的,所以为了简单起见,请在特定实现的上下文中提供解释。
1楼
user3243242
6
已采纳
2015-08-03 03:13:22
这是一个对问题 非常好地回答你的问题的 。
堆栈:
范围与堆栈框架有关(在计算机科学中它称为“激活记录”,但大多数熟悉C或程序集的开发人员更好地将其称为堆栈框架)。 范围是堆栈框架类对象的类型。 我的意思是,在对象是类的实例的情况下,堆栈帧是范围的实例。
我们以一种虚构的语言为例。 在这种语言中,就像在javascript中一样,函数定义范围。 让我们来看一个示例代码:
var global_var function b { var bb } function a { var aa b(); }
当我们读取上面的代码时,我们说变量
aa
在函数a
范围内,变量bb
在函数b
范围内。 请注意,我们不会将此事称为私有变量。 因为私有变量的反义是公共变量,并且都引用绑定到对象的属性。 相反,我们称之为aa
和bb
。 局部变量的反面是全局变量(不是公共变量)。现在,让我们看看当我们调用会发生什么
a
:调用
a()
,创建一个新的堆栈帧。 为堆栈上的局部变量分配空间:The stack: ┌────────┐ │ var aa │ <── a's stack frame ╞════════╡ ┆ ┆ <── caller's stack frame
a()
调用b()
,创建一个新的堆栈帧。 为堆栈上的局部变量分配空间:The stack: ┌────────┐ │ var bb │ <── b's stack frame ╞════════╡ │ var aa │ ╞════════╡ ┆ ┆
在大多数编程语言中,这包括javascript,函数只能访问自己的堆栈帧。 因此
a()
不能访问b()
局部变量,全局范围内的任何其他函数或代码都不能访问a()
变量。 唯一的例外是全局范围内的变量。 从实现的角度来看,这是通过在不属于堆栈的内存区域中分配全局变量来实现的。 这通常称为堆。 所以为了完成图片,此时的内存看起来像这样:The stack: The heap: ┌────────┐ ┌────────────┐ │ var bb │ │ global_var │ ╞════════╡ │ │ │ var aa │ └────────────┘ ╞════════╡ ┆ ┆
(作为旁注,您还可以使用malloc()或new在函数内的堆上分配变量)
现在
b()
完成并返回,它的堆栈帧从堆栈中删除:The stack: The heap: ┌────────┐ ┌────────────┐ │ var aa │ │ global_var │ ╞════════╡ │ │ ┆ ┆ └────────────┘
并且当
a()
完成时,它的堆栈帧发生同样的情况。 这就是局部变量如何自动分配和释放 - 通过从堆栈中推送和弹出对象。闭包:
闭包是一种更先进的堆叠框架。 但是,一旦函数返回,正常的堆栈帧就会被删除,带闭包的语言只会将堆栈帧(或者它包含的对象)从堆栈中取消链接,同时保持对堆栈帧的引用,只要它是必需的。
现在让我们看一下带闭包的语言的示例代码:
function b { var bb return function { var cc } } function a { var aa return b() }
现在让我们看看如果我们这样做会发生什么:
var c = a()
调用第一个函数
a()
,然后调用b()
。 创建堆栈帧并将其推入堆栈:The stack: ┌────────┐ │ var bb │ ╞════════╡ │ var aa │ ╞════════╡ │ var c │ ┆ ┆
函数
b()
返回,因此它的堆栈帧从堆栈中弹出。 但是,函数b()
返回一个匿名函数,它在闭包中捕获bb
。 所以我们弹出堆栈框架,但不要从内存中删除它(直到所有对它的引用都被完全垃圾收集):The stack: somewhere in RAM: ┌────────┐ ┌?????????┐ │ var aa │ ┆ var bb ┆ ╞════════╡ └?????????┘ │ var c │ ┆ ┆
a()
现在将函数返回给c
。 因此,对b()
的调用的堆栈帧被链接到变量c
。 请注意,它是链接的堆栈帧,而不是范围。 有点像从类中创建对象,它是分配给变量的对象,而不是类:The stack: somewhere in RAM: ┌────────┐ ┌?????????┐ │ var c??├???????????┆ var bb ┆ ╞════════╡ └?????????┘ ┆ ┆
另请注意,由于我们实际上没有调用函数
c()
,因此变量cc
尚未在内存中的任何位置分配。 在我们调用c()
之前,它目前只是一个作用域,还不是堆栈帧。现在当我们调用
c()
时会发生什么? 正常创建c()
堆栈帧。 但这次有一点不同:The stack: ┌────────┬──────────┐ │ var cc var bb │ <──── attached closure ╞════════╤──────────┘ │ var c │ ┆ ┆
b()
的堆栈帧附加到c()
的堆栈帧。 所以从函数c()
的角度来看,它的堆栈还包含调用函数b()
时创建的所有变量(再次注意,不是函数b()中的变量,而是函数b()时创建的变量调用 - 换句话说,不是b()的范围,而是调用b()时创建的堆栈框架。暗示只有一个可能的函数b()但是很多调用b()创建了许多堆栈框架。但是局部和全局变量的规则仍然适用。
b()
所有变量都成为c()
局部变量,而不是其他任何变量。 调用c()
的函数无法访问它们。这意味着当您在调用者的范围中重新定义
c
,如下所示:var c = function {/* new function */}
有时候是这样的:
somewhere in RAM: ┌?????????┐ ┆ var bb ┆ └?????????┘ The stack: ┌────────┐ ┌????????????????????┐ │ var c??├???????????┆ /* new function */ ┆ ╞════════╡ └????????????????????┘ ┆ ┆
如您所见,由于
c
所属的范围无法访问它,因此无法从对b()
的调用中重新获得对堆栈帧的访问权限。
2楼
Dmitry Frank
4
2015-09-06 11:27:20
我写了一篇关于这个主题的文章: :图解说明。
要理解这个主题,我们需要知道如何分配,使用和删除范围对象(或LexicalEnvironment
)。
这种理解是了解大局并了解封闭装置如何工作的关键。
我不会在这里重新输入整篇文章,但作为一个简短的例子,请考虑以下脚本:
"use strict";
var foo = 1;
var bar = 2;
function myFunc() {
//-- define local-to-function variables
var a = 1;
var b = 2;
var foo = 3;
}
//-- and then, call it:
myFunc();
执行顶级代码时,我们有以下范围对象的排列:
请注意, myFunc
引用了两个:
- 函数对象(包含代码和任何其他公开可用的属性)
- 定义了由时间函数激活的Scope对象。
当调用myFunc()
时,我们有以下范围链:
调用函数时,将创建新的作用域对象并用于扩充 myFunc
引用的作用域链。
它允许我们在定义一些内部函数时实现非常强大的效果,然后在外部函数之外调用它。
请参阅上述文章,它详细解释了一些内容。
3楼
gmr
1
2015-08-02 19:03:04
下面是一个示例,说明如何将需要闭包的代码转换为不需要闭包的代码。 需要注意的要点是:如何转换函数声明,如何转换函数调用,以及如何转换对已移动到堆的局部变量的访问。
输入:
var f = function (x) {
x = x + 10
var g = function () {
return ++x
}
return g
}
var h = f(3)
console.log(h()) // 14
console.log(h()) // 15
输出:
// Header that goes at the top of the program:
// A list of environments, starting with the one
// corresponding to the innermost scope.
function Envs(car, cdr) {
this.car = car
this.cdr = cdr
}
Envs.prototype.get = function (k) {
var e = this
while (e) {
if (e.car.get(k)) return e.car.get(k)
e = e.cdr
}
// returns undefined if lookup fails
}
Envs.prototype.set = function (k, v) {
var e = this
while (e) {
if (e.car.get(k)) {
e.car.set(k, v)
return this
}
e = e.cdr
}
throw new ReferenceError()
}
// Initialize the global scope.
var envs = new Envs(new Map(), null)
// We have to use this special function to call our closures.
function call(f, ...args) {
return f.func(f.envs, ...args)
}
// End of header.
var f = {
func: function (envs, x) {
envs = new Envs(new Map().set('x',x), envs)
envs.set('x', envs.get('x') + 10))
var g = {
func: function (envs) {
envs = new Envs(new Map(), envs)
return envs.set('x', envs.get('x') + 1).get('x')
},
envs: envs
}
return g
},
envs: envs
}
var h = call(f, 3)
console.log(call(h)) // 14
console.log(call(h)) // 15
让我们分解三个关键转变的方式。
对于函数声明情况,假设我们具有两个参数x
和y
以及一个局部变量z
的函数,并且x
和z
可以转义堆栈帧,因此需要移动到堆。
由于提升,我们可以假设z
在函数的开头声明。
输入:
var f = function f(x, y) {
var z = 7
...
}
输出:
var f = {
func: function f(envs, x, y) {
envs = new Envs(new Map().set('x',x).set('z',7), envs)
...
}
envs: envs
}
这是棘手的部分。
转换的其余部分只是使用call
来调用函数,并使用envs中的查找替换对移动到堆的变量的访问。
几个警告。
我们怎么知道
x
和z
需要移动到堆而不是y
? 答案:最简单的(但可能不是最优的)是将任何东西移动到封闭函数体中引用的堆中。我给出的实现泄漏了大量内存,并且需要函数调用来访问移动到堆的访问本地变量而不是内联。 真正的实现不会做这些事情。
最后,user3856986发布了一个答案,它做出了一些与我不同的假设,所以我们来比较一下。
主要区别在于我假设局部变量将保留在传统堆栈上,而user3856986的答案只有在堆栈将被实现为堆上的某种结构时才有意义(但他或她对此要求不是很明确) )。 像这样的堆实现可以工作,但它会给分配器和GC带来更多负载,因为你必须在堆上分配和收集堆栈帧。 使用现代GC技术,这可能比您想象的更有效,但我相信常用的VM确实使用传统的堆栈。
另外,user3856986的答案中含糊不清的是闭包如何获得对相关堆栈帧的引用。
在我的代码中,当在堆栈帧执行时在闭包上设置envs
属性时会发生这种情况。
最后,user3856986写道,“b()中的所有变量都成为c()的局部变量而不是其他任何东西。调用c()的函数无法访问它们。”
这有点误导。
给定对闭包c
的引用,阻止一个人从调用b
访问闭合变量的唯一方法就是类型系统。
人们可以肯定是从装配访问这些变量(否则,怎么可能c
访问它们?)。
另一方面,对于c
的真正局部变量,在指定c
某个特定调用之前询问是否可以访问它们甚至没有意义(并且如果我们考虑某个特定的调用,则由时间控制返回给调用者,存储在其中的信息可能已经被破坏了。