闭包在何时创建?

13

以下情况下,对于foo创建了闭包,但不对bar创建:

情况1:

<script type="text/javascript">

    function foo() { }

</script>
< p > < code > foo 是一个闭包,它的作用域链只包含全局作用域。 < /p > < p > 情况2: < /p >
<script type="text/javascript">

    var i = 1;
    function foo() { return i; }

</script>

与案例1相同。

案例3:

<script type="text/javascript">

    function Circle(r) {
        this.r = r;
    }
    Circle.prototype.foo = function() { return 3.1415 * this.r * this.r }

</script>
在这种情况下,Circle.prototype.foo(返回圆的面积)指的是一个只有全局作用域的闭包。(该闭包在创建时生成)。
第4种情况:
<script type="text/javascript">

    function foo() { 
        function bar() { 
        }
    }

</script>

在这里,foo是一个仅包含全局作用域的闭包,但是 bar 还不是一个闭包,因为函数 foo 在代码中没有被调用,所以没有闭包 bar 被创建。只有在调用函数 foo 时,闭包 bar 才会存在,而且闭包 bar 将一直存在,直到 foo 返回,之后闭包 bar 将被垃圾回收,因为实际上没有任何地方引用它。

所以当函数不存在、不能被调用或者无法引用时,闭包还不存在(未曾被创建)。只有当函数可以被调用或者可以被引用时,闭包才会被真正地创建?


我相信我们只能推测。这些是实现细节,不是真正的语言特性。 - Ionuț G. Stan
John Resig的新书《JS忍者秘籍》(Secrets of a JS Ninja)对闭包进行了很好的概述 - 他还在他的网站上详细介绍了很多内容(http://ejohn.org/apps/learn/),非常值得一读 - 如果你没有学到任何东西,我会感到惊讶的。 - Martyn
只要你将每个“闭包(closure)”替换为“函数(function)”,你所说的一切都是正确的。有关闭包实际上是什么的解释,请看我的答案。 - Alsciende
5个回答

9
一个闭包是指函数代码中的自由变量通过该函数的“上下文”绑定到某些值(这里闭包比上下文更合适)。
<script type="text/javascript">
    var i = 1;
    function foo() { return i; }
</script>

在这里,i是函数foo的代码中的自由变量。而且这个自由变量没有被任何现有上下文(闭包)绑定到特定的值。因此,您没有任何闭包。

<script type="text/javascript">
    var i = 1;
    function foo() { return i; }
    foo(); // returns 1
    i = 2;
    foo(); // returns 2
</script>

现在要创建一个闭包,你必须提供一个值约束的上下文:
<script type="text/javascript">

    function bar() {
       var i = 1;
       function foo() { return i; }
       return foo;
    }
    bar(); // returns function foo() { return i; }
    bar()(); // returns 1
    // no way to change the value of the free variable i => bound => closure
</script>

简而言之,只有函数返回另一个函数,才能使用闭包。在这种情况下,返回的函数具有在退出函数时存在的所有变量值绑定。

<script type="text/javascript">

    function bar() {
       var i = 1;
       function foo() { return i; }
       i = 2;
       return foo;
    }
    bar()(); // returns 2
</script>

关于你的例子:

  1. 案例1不是闭包,它只是一个函数。
  2. 案例2不是闭包,它是另一个带有自由变量的函数。
  3. 案例3不是闭包,它是另一个带有特殊“变量”this的函数。当该函数作为对象的成员调用时,对象被分配给this的值。否则,this的值为全局对象。
  4. 案例4不是闭包,它是在另一个函数内定义的函数。如果foo返回bar,则会创建一个仅包含'bar'及其值的闭包:function bar() {}

现在考虑程序调用moo.f,moo.f执行“this.g = foo”,然后执行“this.g()”。foo是一个函数并且携带全局作用域的上下文,这不正是闭包的定义吗? - nonopolarity
1
一个函数不能绑定到全局作用域的上下文,因为你永远无法从全局作用域返回。因此它永远不会被关闭,因此你不能在它上面创建闭包。 - Alsciende
1
你需要返回它才能成为闭包吗?我认为你不必返回它。你可以分配它,为什么这很重要呢?我认为闭包的定义是:一些代码与其上下文一起运行,在这种情况下,全局范围将全部适合以上情况。 - nonopolarity
尽管在第二个例子中,如果我在一个完全不同含义的i环境中调用foo,则它仍将引用全局范围中的i。即使我在遮蔽某个其他i的函数中调用它。 - Zorf
我认为你在谈论作用域。闭包不仅仅是作用域,它是一种从其作用域中“提取”函数以冻结值的方式。 - Alsciende
1
“...冻结值” - 但是闭包不会冻结值。闭包中的变量仍然是“活动的”,并且可以被赋予新值。这就是整个意义所在。“除非函数返回另一个函数,否则你不能拥有闭包”- 是的,如果将内部函数的某些引用提供给包含函数之外,则可以。返回内部函数只是其中一种方法(但它可以通过分配或在回调参数中发生,例如)。 - nnnnnn

1

闭包 bar 将一直存在,直到 foo 返回,然后闭包 bar 将被垃圾回收,因为在任何地方都没有对它的引用。

是的。


但如果 foo 从来没有被调用,那么闭包 bar 就从未被创建? - nonopolarity

0

实际上,经过几年的JavaScript使用和相当深入的研究,我现在有了更好的答案:

每当一个函数被创建时,就会创建一个闭包。

因为函数只是一个对象,我们可以更精确地说,每当一个Function对象被实例化(函数实例被创建)时,就会创建一个闭包。

所以,

function foo() { }

当 JS 运行上述代码时,已经存在一个闭包,或者
var fn = function() { };

或者

return function() { return 1; };

为什么?因为闭包只是一个具有作用域链的函数,所以在上述每种情况下都存在一个函数(它已经存在。您可以调用它(调用它))。它还具有作用域。因此,在我的原始问题中(我是OP),每个Case 1到4都创建了一个闭包,在每种情况下都是如此。
第4种情况是一个有趣的情况。运行该代码后,由于foo()的存在,会产生一个闭包,但是bar()尚不存在(没有调用foo()),因此只创建了一个闭包,而不是两个。

0
在这些例子中,都没有创建闭包。
如果您实际上创建了一个函数并对其进行操作,第二个示例将创建一个闭包,现在您只是创建一个函数然后将其丢弃。就像添加一行3+8;一样,您创建一个数字,然后将其丢弃。
闭包只是一个函数,在其主体中引用其创建环境中的变量,一个经典的例子是加法器:
function createAdder(x) { //this is not a closure
    return function(y) { //this function is the closure however, it closes over the x.
        return y + x;
    }
} //thus createAdder returns a closure, it's closed over the argument we put into createAdder

var addTwo = createAdder(2);

addTwo(3); //3

0
如果我可以提供一个模型来说明闭包何时以及如何创建(这个讨论是理论性的,在现实中,解释器可能会做任何事情,只要最终结果相同):每当在执行期间评估函数时,都会创建一个闭包。然后,闭包将指向执行发生的环境。当网站加载时,Javascript 在全局环境中按从上到下的顺序执行。所有出现的情况都是闭包。
function f(<vars>) {
  <body>
}

将被转化为一个闭包,并带有和,指向全局环境的指针。同时,在全局环境中创建一个指向此闭包的引用f

那么当在全局环境执行f()时会发生什么呢?我们可以将其想象为首先在全局环境(函数正在执行的地方)中查找名称f。我们发现它指向一个闭包。要执行该闭包,我们创建一个新环境,其父环境是由闭包f指向的环境,即全局环境。在这个新环境中,我们将f的参数与其实际值关联起来。然后在新环境中执行闭包f的主体!任何f所需的变量都将优先在我们刚刚创建的新环境中解析。如果不存在这样的变量,则我们递归地在父环境中查找,直到我们找到全局环境。任何f创建的变量都将在新环境中创建。

现在,让我们来看一个更复杂的例子:

// At global level
var i = 10;                  // (1)
function make_counter(start) {
  return function() {
    var value = start++;
    return value;
  };
}                            // (2)
var count = make_counter(10);    // (3)
count();  // return 10       // (4)
count();  // return 11       // (5)
count = 0;                   // (6)

发生的情况是:

在点(1):在全局环境中建立了从i10的关联(执行var i = 10;)。

在点(2):使用变量(start)和主体return ...;创建了一个闭包,该闭包指向正在执行的环境(全局)。然后,将make_counter与我们刚刚创建的闭包建立了关联。

在第三步骤中,发生了几件有趣的事情。首先,我们找到了全局环境中与make_counter相关联的内容。然后我们执行该闭包。因此,创建了一个新的环境,让我们称其为CE,它指向由闭包make_counter(全局)指向的环境。然后,在CE中创建了从start10的关联,并在CE中运行闭包make_counter的主体。在这里,我们遇到了另一个函数,它是匿名的。但是,发生的事情与之前相同(回想一下function f() {}等价于var f = function() {};)。创建了一个闭包,让我们称其为count,其中变量()(空列表)和主体var ... return value;。现在,这个闭包将指向它正在执行的环境,即CE。这将在以后非常重要。最后,我们让count指向全局环境中的新闭包(为什么是全局?因为var count ...在全局环境中执行)。我们注意到,CE没有被垃圾回收,因为我们可以通过变量make_counter从全局环境中访问闭包make_counter,从而访问CE
在第4点,更有趣的事情发生了。我们首先找到与count相关联的闭包,这是我们刚刚创建的闭包。然后我们创建一个新的环境,其父级是闭包指向的环境,即CE!我们在这个新环境中执行闭包的主体。当执行var value = start++;时,我们从当前环境开始向上移动,顺序查找变量start,直到全局环境为止。我们在环境CE中找到了start。我们将此start的值从最初的10增加到11。现在,在CE中的start指向值11。当我们遇到var value时,这意味着不要费劲寻找现有的value,只需在执行它的环境中创建一个变量。因此,建立了从value11的关联。在return value;中,我们以与查找start相同的方式查找value。结果发现它在当前环境中,因此我们不需要查找父级环境。然后我们返回这个值。现在,我们刚刚创建的新环境将被垃圾收集,因为我们无法通过任何路径从全局访问此环境。

在第5点,与上面发生了相同的事情。但是现在,当我们寻找start时,我们发现值为11而不是10(在环境CE中)。

在第6点,我们在全局环境中重新分配count。我们发现现在我们无法再从全局到闭包count找到路径,因此我们也无法再找到通向环境CE的路径。因此,这两者都将被垃圾回收。

P.S. 对于熟悉LISP或Scheme的人来说,上面的模型与LISP / Scheme中的环境模型完全相同。

P.P.S. 哇,起初我想写一个简短的答案,但结果变成了这个庞然大物。我希望我没有犯什么明显的错误。


那么您认为原问题中的1到4案例也是闭包吗?在研究了Ruby或Lisp中闭包的概念后,我同意您的观点。 - nonopolarity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接