JS 里闭包是如何 capture 外部变量的?

2019-08-30 15:16:21 +08:00
FaiChou  FaiChou
function foo() {
  var a = 1;
  return function() {
    console.log(a);
  }
}
var f = foo() // line 7 capture a ? why
f = undefined // line 8 release a

这一段代码, 在 js 引擎执行到 7 行时候, 会在 global execution context 上面创建一个 foo 的 execution context , 这时候 foo 被执行, 当 foo 执行完后, 返回一个闭包给变量 f, 这时候闭包函数未被执行, 为何还会 capture 外部的变量 a 呢? 闭包函数不执行, 应该不会有 VariableEnvironment 和 LaxicalEnvrironment 吧, 所以闭包是怎么 capture 外部变量的呢? 我理解的哪里有问题吗?

4261 次点击
所在节点   JavaScript  JavaScript
11 条回复
s0f
s0f
2019-08-30 15:21:37 +08:00
函数在创建的时候已经引用了外部的上下文,和这个函数的执不执行没关系。
maichael
maichael
2019-08-30 15:22:09 +08:00
"capture"是发生在函数定义的时候,而不是执行的时候,不然每一次执行“ capture ”的变量都不一样了,闭包的意义在那。
FaiChou
FaiChou
2019-08-30 15:30:54 +08:00
@s0f
也就是说执行到第 6 行时, 已经有了 3 个 context:

global execution context, foo execution context, closure execution context 分别对应:

GlobalExecutionContext = {
ThisBinding: global,
VariableEnvironment: { },
LexicalEnvironment: { }
}
FooExecutionContext = {
ThisBinding: foo,
VariableEnvironment: { a: undefined },
LexicalEnvironment: { }
}
ClosureExecutionContext = {
ThisBinding: closure,
VariableEnvironment: { },
LexicalEnvironment: { a: undefined }
}

在执行完第 7 行, 虽然 FooExecutionContext 被弹出栈, js 运行时发现变量 a 还有 closure 的 lexicalEnvironment 引用, 所以不会释放变量 a, 对吗?
FaiChou
FaiChou
2019-08-30 15:39:29 +08:00
@maichael

https://hackernoon.com/javascript-execution-context-and-lexical-environment-explained-528351703922
这篇文章有一句:

> Each time you invoked a function it will create a new Function Execution Context.

也就是说, 只有在函数被执行(invoke)时候, 才会创建 execution context.

这句是对的吗?

函数定时时候就 'capture' 外部变量, 此时函数没有被执行, 也就没有 context, 那么应该怎么解释 capture ?
mcfog
mcfog
2019-08-30 15:55:13 +08:00
执行( enter ) function 的时候的过程在这里,确实会新建 execution context

https://www.ecma-international.org/ecma-262/5.1/#sec-10.4.3

注意

> Let localEnv be the result of calling NewDeclarativeEnvironment passing the value of the [[Scope]] internal property of F as the argument.

这里,后续用来创建 context 的 localEnv 这个东西来自于[[Scope]]这个内部属性,然后这个属性是在创建函数的时候绑定的,参考这里

https://www.ecma-international.org/ecma-262/5.1/#sec-13

关注里面关于 Scope 的描述

在执行 foo,return 前,计算 function() {
console.log(a);
}
表达式的值的时候,当前的 lexical environment 也就是包含 var a=1 的东西被记录在这个函数的[[Scope]]内部属性中了,于是后面有
f.[[Scope]] => Lexical Environment{ a=1 }
阻止了 a 被 GC
maichael
maichael
2019-08-30 15:55:33 +08:00
@FaiChou #4 在 foo 里面的匿名函数定义时,匿名函数处于 foo 的 execution context 中,它自然可以 capture 这个外部变量。

https://www.cnblogs.com/starof/p/6400261.html
mcfog
mcfog
2019-08-30 15:58:44 +08:00
@mcfog 更正一下后面,精确地说,应该是
f.[[Scope]] => DeclarativeEnvironment { ..., outer => Lexical Environment{ a=1 } }

如果你的 console.log(a)旁边有 var x=42,x=42 就是在...位置的
s0f
2019-08-30 16:04:40 +08:00
@FaiChou 就是这样。函数执行会创建上下文没错,但是函数创建的时候,按照 ES3 的解释,会创建好包含外部作用域,保存在函数的 [[Scope]] 中,等到调用时再复制到上下文中。ES5
s0f
2019-08-30 16:05:22 +08:00
@s0f 擦...还没打完,按错键直接就发送了
v2qwsdcv
2019-08-30 17:56:55 +08:00
按引用
function f(){var a ={abc:1}; var o=function(){return a};a.abc=3;return o;}
f()() //{abc: 3}
function f(){var a =1; var o=function(){a=2;return a};return o;}
f()()//2
相当于 C++的 [&](){...}
rus4db
2019-09-05 23:37:33 +08:00
一个函数,如果其作用域内不引用任何“外面的”变量,称为“封闭的”。
但如果一个函数不是封闭的,那么其内部引用的“外面的”变量,称为它的“自由变量”。
闭包就是保存了自由变量绑定的函数实例。

为什么要保存定义时所在上下文的自由变量绑定?因为 JS 是词法作用域的。
所谓词法作用域,就是指匿名函数里面的自由变量`a`,不会因为这个匿名函数到了另外一个定义了`a`的环境中去执行,而变成了另外一个值。为了维护这种“词法上的”继承关系,匿名函数实例必须保存定义处(而不是执行处)环境的自由变量绑定。这就是 closure,闭包。

至于`var`,它具有所谓的提升特性。可以理解成是定义了在作用域内部任何位置都有效的准全局变量,这与 Scheme 的`letrec`很类似。

p.s.
① 关于词法作用域和闭包,推荐阅读王垠的文章: http://www.yinwang.org/blog-cn/2012/08/01/interpreter
② 安利 Racket 或者 Scheme 语言。JS 就是受到 Scheme 的启发而设计出的语言。JS 很多看似奇怪的特性,实际上都来源于 Scheme 这门函数式语言。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/596547

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX