我知道闭包被定义为:
[A]堆栈帧,当函数返回时未解除分配。 (好像'stack-frame'是malloc'ed而不是堆栈!)
但我不明白这个答案如何适应JavaScript的存储机制。口译员如何跟踪这些价值观?浏览器的存储机制是否以类似于堆和堆栈的方式进行分段?
关于这个问题的答案:How do JavaScript closures work?解释:
[A]函数引用也有一个对闭包的秘密引用
这个神秘的“秘密参考”背后的潜在机制是什么?
EDIT 许多人说这是依赖于实现的,因此为了简单起见,请在特定实现的上下文中提供解释。
答案 0 :(得分:4)
这是问题slebetman's answer javascript can't access private properties的一部分,可以很好地回答您的问题。
筹码:
范围与堆栈框架有关(在计算机科学中它被称为 "激活记录"但大多数开发人员熟悉C或 装配比堆叠框架更好地了解它。范围是堆栈帧 一个类对象是什么。我的意思是说对象在哪里 一个类的实例,一个堆栈框架是范围的实例。
让我们用一种虚构的语言作为例子。用这种语言,就像在 javascript,函数定义范围。让我们来看一个例子 代码:
var global_var function b { var bb } function a { var aa b(); }
当我们阅读上面的代码时,我们说变量
aa
在范围内 在函数a
中,变量bb
在函数b
的范围内。 请注意,我们不会将此事称为私有变量。因为 私有变量的对面是公共变量,两者都是指 绑定到对象的属性。相反,我们会调用aa
和bb
local variables。局部变量的反面是全局变量 (不是公共变量)。现在,让我们看看当我们致电
调用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 函数只能访问自己的堆栈帧。因此
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
尚未在内存中的任何位置分配。它&#39; S 在我们调用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 */ ┆ ╞════════╡ └╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶┘ ┆ ┆
如您所见,重新获得对堆栈框架的访问是不可能的 由于
b()
所属的范围不属于c
,因此可以致电{{1}} 可以访问它。
答案 1 :(得分:3)
我写了一篇关于这个主题的文章:How do JavaScript closures work under the hood:图解说明。
要理解主题,我们需要知道范围对象(或LexicalEnvironment
s)的分配,使用和删除方式。这种理解是了解大局并了解封闭装置如何工作的关键。
我不会在这里重新输入整篇文章,但作为一个简短的例子,请考虑这个脚本:
"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
引用了两者:
当调用myFunc()
时,我们有以下范围链:
调用函数时,会创建新的范围对象并用于扩充 myFunc
引用的范围链。它允许我们在定义一些内部函数时实现非常强大的效果,然后在外部函数之外调用它。
参见上述文章,它详细解释了一些内容。
答案 2 :(得分:1)
以下是一个示例,说明如何将需要闭包的代码转换为不具备代码的代码。需要注意的要点是:如何转换函数声明,如何转换函数调用,以及如何转换对已移动到堆的局部变量的访问。
输入:
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技术,这可能比您想象的更有效,但我相信常用的虚拟机确实使用传统的堆栈。
另外,在user3856986的回答中,模糊不清的是闭包如何获得对相关堆栈帧的引用。在我的代码中,当在堆栈帧执行时在闭包上设置envs
属性时会发生这种情况。
最后,user3856986写道,&#34; b()中的所有变量都成为c()的局部变量,而不是其他任何变量。调用c()的函数无法访问它们。&#34;这有点误导。给定对闭包c
的引用,阻止一个人从调用b
访问闭合变量的唯一因素是类型系统。人们当然可以从汇编中访问这些变量(否则,c
如何访问它们?)。另一方面,对于c
的真实局部变量,在指定c
的某个特定调用之前,询问是否可以访问它们甚至没有意义(如果我们考虑某个特定的呼叫,那么当控制回到呼叫者时,存储在其中的信息可能已经被破坏了。