V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
FaiChou
V2EX  ›  JavaScript

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

  •  
  •   FaiChou · 2019-08-30 15:16:21 +08:00 · 4213 次点击
    这是一个创建于 1914 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 外部变量的呢? 我理解的哪里有问题吗?

    第 1 条附言  ·  2019-09-01 21:59:16 +08:00

    https://astronautweb.co/javascript-lexical-scope/

    问题的关键是这个lexical scope.

    比如这样一段代码:

    var a = 1
    console.log(a)
    
    console.log(b)
    var b = 2
    
    console.log(c)
    

    运行结果是:

    1
    undefinded
    ReferenceError: c is not defined
    

    从这里可以看出, js语言在运行之前,也就是compile阶段, 就已经将变量定义好了, 只不过赋值是在运行时, 这也就是人们常说的js变量提升的特性.

    所以本帖中的问题 js闭包如何捕获外部变量 可以这么解释: 在line7执行前已经被两个scope捕获: foo 和 闭包, 也就是大家讨论的 LexicalEnvironment.

    11 条回复    2019-09-05 23:37:33 +08:00
    s0f
        1
    s0f  
       2019-08-30 15:21:37 +08:00   ❤️ 1
    函数在创建的时候已经引用了外部的上下文,和这个函数的执不执行没关系。
    maichael
        2
    maichael  
       2019-08-30 15:22:09 +08:00   ❤️ 1
    "capture"是发生在函数定义的时候,而不是执行的时候,不然每一次执行“ capture ”的变量都不一样了,闭包的意义在那。
    FaiChou
        3
    FaiChou  
    OP
       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
        4
    FaiChou  
    OP
       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
        5
    mcfog  
       2019-08-30 15:55:13 +08:00   ❤️ 1
    执行( 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
        6
    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
        7
    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
        8
    s0f  
       2019-08-30 16:04:40 +08:00
    @FaiChou 就是这样。函数执行会创建上下文没错,但是函数创建的时候,按照 ES3 的解释,会创建好包含外部作用域,保存在函数的 [[Scope]] 中,等到调用时再复制到上下文中。ES5
    s0f
        9
    s0f  
       2019-08-30 16:05:22 +08:00
    @s0f 擦...还没打完,按错键直接就发送了
    v2qwsdcv
        10
    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
        11
    rus4db  
       2019-09-05 23:37:33 +08:00   ❤️ 1
    一个函数,如果其作用域内不引用任何“外面的”变量,称为“封闭的”。
    但如果一个函数不是封闭的,那么其内部引用的“外面的”变量,称为它的“自由变量”。
    闭包就是保存了自由变量绑定的函数实例。

    为什么要保存定义时所在上下文的自由变量绑定?因为 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 这门函数式语言。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3301 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 12:25 · PVG 20:25 · LAX 04:25 · JFK 07:25
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.