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

JavaScript闭包如何在低级别工作?,javascript,browser,closures,Javascript,Browser,Closures,我理解,关闭的定义如下: [A] 函数返回时未释放的堆栈帧。(好像一个“堆栈帧”是malloc'ed而不是在堆栈上!) 但我不明白这个答案如何适用于JavaScript的存储机制。解释器如何跟踪这些值?浏览器的存储机制是否以类似于堆和堆栈的方式分段 关于这个问题的答案是:说明: [A] 函数引用还有一个对闭包的秘密引用 这种神秘的“秘密参照”背后的潜在机制是什么 编辑 很多人都说这取决于实现,因此为了简单起见,请在特定实现的上下文中提供解释 下面是一个示例,说明如何将需要闭包的代码转换为不需要闭

我理解,关闭的定义如下:

[A] 函数返回时未释放的堆栈帧。(好像一个“堆栈帧”是malloc'ed而不是在堆栈上!)

但我不明白这个答案如何适用于JavaScript的存储机制。解释器如何跟踪这些值?浏览器的存储机制是否以类似于堆和堆栈的方式分段

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

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

这种神秘的“秘密参照”背后的潜在机制是什么

编辑
很多人都说这取决于实现,因此为了简单起见,请在特定实现的上下文中提供解释

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

输入:

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
var f = function f(x, y) {
    var z = 7
    ...
}
输出:

// 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
var f = {
    func: function f(envs, x, y) {
        envs = new Envs(new Map().set('x',x).set('z',7), envs)
        ...
    }
    envs: envs
}
让我们分析一下三个关键转换是如何进行的。对于函数声明情况,为具体起见,假设我们有一个函数,它有两个参数
x
y
和一个局部变量
z
,并且
x
z
可以逃逸堆栈帧,因此需要移动到堆中。由于提升的原因,我们可以假设
z
是在函数的开头声明的

输入:

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
var f = function f(x, y) {
    var z = 7
    ...
}
输出:

// 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
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
    的某个特定调用之前,询问您是否可以访问它们是没有意义的(如果我们考虑一些特定的调用,通过时间控制回到调用方,存储在它们中的信息可能已经被破坏)。 堆栈: 作用域与堆栈框架相关(在计算机科学中称为 “激活记录”,但大多数开发人员熟悉C或 程序集更熟悉堆栈框架)。作用域是堆栈框架的一部分 类与对象的关系,我的意思是对象所在的位置 堆栈框架是类的实例,堆栈框架是范围的实例

    让我们以一种虚构的语言为例 javascript,函数定义范围。让我们看一个例子 代码:

    当我们阅读上面的代码时,我们说变量
    aa
    在范围内 在函数
    a
    中,变量
    bb
    在函数
    b
    的范围内。 注意,我们不把它称为私有变量 私有变量的对立面是公共变量,两者都指 属性绑定到对象。相反,我们称为
    aa
    bb
    。局部变量的反面是全局变量 (不是公共变量)

    现在,让我们看看调用
    a
    时会发生什么:

    a()
    被调用时,创建一个新的堆栈帧。为本地 堆栈上的变量:

    堆栈:
    ┌────────┐
    
    │ var aa│ 我写了一篇关于这个主题的文章:图解说明

    要理解这个主题,我们需要知道范围对象(或
    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();
    
    在执行顶层