闭包中外部作用域变量在ExecutionContext的存储位置及LexicalEnvironment实现问询
核心问题
- 查阅大量LexicalEnvironment资料后,未找到闭包在此层面的实现细节:外部函数执行完毕、ExecutionContext销毁后,闭包保留的LexicalEnvironment数据存储在何处?
- FunctionObject能否同时保留多个LexicalEnvironment?
- 以下闭包的上下文示例是否合理?
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", a: < uninitialized >, b: < uninitialized >, multiply: < func > }, outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", c: undefined, }, outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", g: undefined }, outer: <GlobalLexicalEnvironment> } }
用户补充:已研读ECMA规范及相关视频,找到的清晰描述为:
当外部函数执行完毕,其上下文会被销毁。同时,由于内部(闭包)函数需要访问其变量,外部函数的LexicalEnvironment会被保留。
或:
即便执行上下文在运行完内部所有代码后从栈中删除,当原函数返回一个使用其作用域外变量的新函数时,该函数的引用仍处于活跃状态。此时,与外部函数的链接直到其代码执行完毕并完全从栈中移除才会被删除。这就是闭包。闭包是一种即便父函数执行阶段已结束,仍能访问父函数作用域的函数。
Chrome调试时可看到Closure作用域。
解答
1. 外部函数上下文销毁后,LexicalEnvironment的存储位置
外部函数的ExecutionContext会从调用栈中弹出销毁,但它的LexicalEnvironment(准确说是其中的EnvironmentRecord)会作为闭包的一部分,存储在内部函数的FunctionObject中。
在JS引擎实现中,每个FunctionObject都有一个[[Environment]]内部槽(ECMA规范定义),这个槽指向函数创建时所在的LexicalEnvironment。只要内部函数还存在引用(比如被返回、赋值给全局变量),这个[[Environment]]引用就会阻止外部函数的LexicalEnvironment被垃圾回收,它会留在堆内存中,而非调用栈里。Chrome调试中看到的"Closure"作用域,就是这个堆中保留的LexicalEnvironment的可视化展示。
2. FunctionObject能否同时保留多个LexicalEnvironment?
不能。每个FunctionObject的[[Environment]]内部槽只能指向一个LexicalEnvironment——也就是该函数创建时所处的直接外层LexicalEnvironment。
不过,LexicalEnvironment本身带有outer指针,指向更外层的LexicalEnvironment,形成作用域链,所以闭包可以通过链式结构访问多层外层环境的变量。比如:
function outer() { const x = 1; function middle() { const y = 2; return function inner() { console.log(x + y); }; } return middle(); } const closure = outer(); closure(); // 输出3
这里inner函数的[[Environment]]指向middle的LexicalEnvironment,而middle的LexicalEnvironment的outer指向outer的LexicalEnvironment,所以inner能访问x和y,但每个函数只直接关联一个LexicalEnvironment。
3. 给出的上下文示例合理性分析
你的示例基本符合ECMA规范中ExecutionContext的结构,但有几个细节需要修正:
- 结构上缺少必要逗号(比如LexicalEnvironment的EnvironmentRecord和outer之间),语法需补全;
- 示例未体现内部闭包函数与外部函数LexicalEnvironment的关联,若要展示闭包场景,需补充内部函数的[[Environment]]指向外部函数的LexicalEnvironment这一逻辑。
另外,函数执行上下文初期,VariableEnvironment和LexicalEnvironment指向同一个环境记录,只有当遇到let/const声明时,LexicalEnvironment才会更新,而VariableEnvironment始终保存函数执行初期的环境记录(用于var声明的变量)。你的示例中两者指向不同环境记录,这仅在函数内同时存在var和let/const时出现,若只是展示结构,是合理的。
内容的提问来源于stack exchange,提问作者Kostya Tkachenko

