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

时间:2015-07-30 22:26:54

标签: javascript browser closures

我知道闭包被定义为:

  

[A]堆栈帧,当函数返回时未解除分配。 (好像'stack-frame'是malloc'ed而不是堆栈!)

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

关于这个问题的答案:How do JavaScript closures work?解释:

  

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

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

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

3 个答案:

答案 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的范围内。   请注意,我们不会将此事称为私有变量。因为   私有变量的对面是公共变量,两者都是指   绑定到对象的属性。相反,我们会调用aabb 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();

执行顶级代码时,我们有以下范围对象的排列:

enter image description here

请注意myFunc引用了两者:

  • 功能对象(包含代码和任何其他公开可用的属性)
  • 定义了时间函数激活的Scope对象。

当调用myFunc()时,我们有以下范围链:

enter image description here

调用函数时,会创建新的范围对象并用于扩充 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

让我们分解三个关键转变的方式。对于函数声明案例,假设我们具有两个参数xy以及一个局部变量zxz的函数的具体性可以逃离堆栈框架,因此需要移动到堆。由于提升,我们可以假设在函数的开头声明了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. 我给出的实现泄漏了大量内存,并且需要函数调用来访问移动到堆中的访问本地变量而不是内联。一个真正的实现不会做这些事情。

  3. 最后,user3856986发布了一个答案,它做出了与我不同的假设,所以让我们进行比较。

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

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

    最后,user3856986写道,&#34; b()中的所有变量都成为c()的局部变量,而不是其他任何变量。调用c()的函数无法访问它们。&#34;这有点误导。给定对闭包c的引用,阻止一个人从调用b访问闭合变量的唯一因素是类型系统。人们当然可以从汇编中访问这些变量(否则,c如何访问它们?)。另一方面,对于c的真实局部变量,在指定c的某个特定调用之前,询问是否可以访问它们甚至没有意义(如果我们考虑某个特定的呼叫,那么当控制回到呼叫者时,存储在其中的信息可能已经被破坏了。