当前位置: 代码迷 >> JavaScript >> JavaScript闭包如何在低级别工作?
  详细解决方案

JavaScript闭包如何在低级别工作?

热度:65   发布时间:2023-06-05 11:39:33.0

我知道闭包被定义为:

[A]堆栈帧,当函数返回时不会释放。 (好像'stack-frame'是malloc'ed而不是堆栈!)

但我不明白这个答案如何适应JavaScript的存储机制。 口译员如何跟踪这些价值观? 浏览器的存储机制是否以类似于堆和堆栈的方式进行分段?

关于这个问题的答案: 说明:

[A]函数引用也有一个关闭的秘密引用

这个神秘的“秘密参考”背后的潜在机制是什么?

编辑许多人说这是依赖于实现的,所以为了简单起见,请在特定实现的上下文中提供解释。

这是一个对问题 非常好地回答你的问题的 。

堆栈:

范围与堆栈框架有关(在计算机科学中它称为“激活记录”,但大多数熟悉C或程序集的开发人员更好地将其称为堆栈框架)。 范围是堆栈框架类对象的类型。 我的意思是,在对象是类的实例的情况下,堆栈帧是范围的实例。

我们以一种虚构的语言为例。 在这种语言中,就像在javascript中一样,函数定义范围。 让我们来看一个示例代码:

 var global_var function b { var bb } function a { var aa b(); } 

当我们读取上面的代码时,我们说变量aa在函数a范围内,变量bb在函数b范围内。 请注意,我们不会将此事称为私有变量。 因为私有变量的反义是公共变量,并且都引用绑定到对象的属性。 相反,我们称之为aabb 局部变量的反面是全局变量(不是公共变量)。

现在,让我们看看当我们调用会发生什么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()的调用中重新获得对堆栈帧的访问权限。

我写了一篇关于这个主题的文章: :图解说明。

要理解这个主题,我们需要知道如何分配,使用和删除范围对象(或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引用的作用域链。 它允许我们在定义一些内部函数时实现非常强大的效果,然后在外部函数之外调用它。

请参阅上述文章,它详细解释了一些内容。

下面是一个示例,说明如何将需要闭包的代码转换为不需要闭包的代码。 需要注意的要点是:如何转换函数声明,如何转换函数调用,以及如何转换对已移动到堆的局部变量的访问。

输入:

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

让我们分解三个关键转变的方式。 对于函数声明情况,假设我们具有两个参数xy以及一个局部变量z的函数,并且xz可以转义堆栈帧,因此需要移动到堆。 由于提升,我们可以假设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中的查找替换对移动到堆的变量的访问。

几个警告。

  1. 我们怎么知道xz需要移动到堆而不是y 答案:最简单的(但可能不是最优的)是将任何东西移动到封闭函数体中引用的堆中。

  2. 我给出的实现泄漏了大量内存,并且需要函数调用来访问移动到堆的访问本地变量而不是内联。 真正的实现不会做这些事情。

最后,user3856986发布了一个答案,它做出了一些与我不同的假设,所以我们来比较一下。

主要区别在于我假设局部变量将保留在传统堆栈上,而user3856986的答案只有在堆栈将被实现为堆上的某种结构时才有意义(但他或她对此要求不是很明确) )。 像这样的堆实现可以工作,但它会给分配器和GC带来更多负载,因为你必须在堆上分配和收集堆栈帧。 使用现代GC技术,这可能比您想象的更有效,但我相信常用的VM确实使用传统的堆栈。

另外,user3856986的答案中含糊不清的是闭包如何获得对相关堆栈帧的引用。 在我的代码中,当在堆栈帧执行时在闭包上设置envs属性时会发生这种情况。

最后,user3856986写道,“b()中的所有变量都成为c()的局部变量而不是其他任何东西。调用c()的函数无法访问它们。” 这有点误导。 给定对闭包c的引用,阻止一个人从调用b访问闭合变量的唯一方法就是类型系统。 人们可以肯定是从装配访问这些变量(否则,怎么可能c访问它们?)。 另一方面,对于c的真正局部变量,在指定c某个特定调用之前询问是否可以访问它们甚至没有意义(并且如果我们考虑某个特定的调用,则由时间控制返回给调用者,存储在其中的信息可能已经被破坏了。

  相关解决方案