深入理解 Javascript 中的闭包

Posted

技术标签:

【中文标题】深入理解 Javascript 中的闭包【英文标题】:deeper understanding of closure in Javascript 【发布时间】:2013-03-25 22:51:50 【问题描述】:

我正在阅读 cmets 的答案并看到 this comment:

[闭包] 不会持久化 foo 的状态,而是创建一个包含 (1) 返回函数和 (2) 返回时引用的所有外部变量的特殊范围。这种特殊的作用域称为闭包。

好的,到目前为止一切顺利。现在这是我不知道的有趣部分:

恰当的例子...如果您在 foo 中定义了另一个在返回函数中未引用的 var,则它不会存在于闭包范围内。

我想这是有道理的,但这除了内存使用/性能之外还有什么含义

问题 -- 如果范围内的所有变量都包含在闭包中,那我可以做哪些我无法用当前模型做的事情?

【问题讨论】:

***.com/a/12931785/783743 第二个引用不一定正确。至少在理论上存在一个“未使用”的闭包,但它可能会被删除(比如为了优化),因为它没有被使用。 ECMA-262 没有定义实现,只定义行为,所以如果一个闭包从未使用过,你怎么知道它是否存在? :-) ECMAScript 中的 每个 函数都可以为其外部执行上下文创建一个闭包,至少是全局上下文。 为什么这个问题被标记为重复?自从它被回答/接受以来没有真正的伤害,但它确实是一个与链接问题不同的问题。并非所有关于 Stack Overflow 的闭包问题都可以与“闭包如何工作?”重复。在这种情况下,对该问题的最高投票答案并未直接解决该问题所关注的问题。我希望没有人足够关心重新开放,但关闭对我来说似乎很仓促。 它没有被标记为重复 - 这只是指向该答案的链接。 【参考方案1】:

我认为你对这个评论的理解太过分了。该评论只是说您无法在函数范围之外访问它(它不可公开访问),而不是在函数内根本不可用。无论如何,返回的函数都可以访问所有外部函数范围。如果内部函数不提供访问它的方法,您就无法在外部函数之外访问该范围。

例如,这个表达式的计算结果为 4:

function testClosure()
 var x = 2;
    return function(y)
        alert(eval(y));
    



var closure = testClosure();

closure("x+2");  //4

http://jsfiddle.net/dmRcH/

所以x 是可用的,尽管没有被直接引用

进一步研究

看来,chrome 和 firefox 至少确实会尝试对此进行优化,因为如果您不提供任何方法来引用 x 变量,则它不会在调试器中显示为可用。在闭包内使用断点运行它会显示 x 在 Chrome 26 和 Firefox 18 上不可用。

http://jsfiddle.net/FgekX/1/

但这只是内存管理细节,而不是语言的相关属性。如果有任何可能的方式可以引用该变量,它就会被传递,我怀疑其他浏览器可能不会以同样的方式优化它。编写规范总是比编写实现更好。在这种情况下,虽然规则确实是:“如果您有任何可能的方式访问它,它就会可用”。另外,不要使用 eval,因为它确实会让你的代码无法优化任何东西

【讨论】:

据我所知,您的回答只是表明引用的“案例”部分是错误的 不,我认为评论完全是错误的;它明确指出只有在闭包内部被引用的变量被添加到作用域中,这是不正确的。 我同意(作为发表该评论的人)。这违背了我对关闭的理解。 +1 我猜这是“eval is evil”的一个例子,因为它阻止了很多优化。【参考方案2】:

如果您在 foo 中定义了另一个未在返回函数中引用的 var,则它不会存在于闭包范围内。

这并不完全准确;该变量是闭包范围的一部分,即使它可能不会在函数本身内部直接引用(通过查看函数代码)。不同之处在于引擎如何优化未使用的变量。

例如,当您使用 DOM 元素时,已知闭包范围内未使用的变量会导致内存泄漏(在某些引擎上)。以这个经典的例子为例:

function addHandler() 
    var el = document.getElementById('el');
    el.onclick = function() 
        this.style.backgroundColor = 'red';
    

Source

在上面的代码中,内存泄漏(至少在 IE 和 Mozilla 中)因为el 是单击处理函数的闭包范围的一部分,即使它没有被引用;这会导致无法删除的循环引用,因为它们的垃圾收集是基于引用计数的。

另一方面,Chrome 使用 different garbage collector:

在 V8 中,对象堆被分成两部分:创建对象的新空间和在垃圾回收周期中幸存的对象被提升到的旧空间。如果一个对象在垃圾回收周期中被移动,V8 会更新所有指向该对象的指针。

这也称为世代或ephemeral garbage collector。虽然比较复杂,但这种垃圾收集器可以更准确地确定变量是否被使用。

【讨论】:

有趣的是,消息来源说这会在 IE 中泄漏surprise惊奇,但它会在真实浏览器中泄漏吗? @mkoryak 是的,它在 IE 中创建了一个循环引用,并说 GC 是基于引用计数的(不是那么聪明);我现在还不清楚真正的浏览器(或者实际上是 IE 本身)是否有更好的 GC。事实上,我可能也想知道:) @mkoryak 设法找到了一些关于 V8 垃圾收集的信息,这些信息可能对你有用:)【参考方案3】:

javascript 没有固有的隐私感,因此我们使用函数范围(闭包)来模拟此功能。

您引用的 SO 答案是模块模式的一个示例,Addy Osmani 很好地解释了 Learning JavaScript Design Patterns 中的重要性:

模块模式封装了“privacy”、状态和组织 使用闭包。它提供了一种包装公共和 私有方法和变量,防止碎片泄漏到 全局范围并意外与其他开发人员的冲突 界面。使用这种模式,只返回一个公共 API,保持 闭包中的所有其他内容都是私有的。

【讨论】:

我没有问什么是闭包:)【参考方案4】:
    Please have a look below code:

    for(var i=0; i< 5; i++)            
                setTimeout(function()
                    console.log(i);
                ,1000);                        
            

   Here what will be output? 0,1,2,3,4 not that will be 5,5,5,5,5 because of closure

   So how it will solve? Answer is below:

   for(var i=0; i< 5; i++)
            (function(j)     //using IIFE           
                setTimeout(function()
                    console.log(j);
                ,1000);
            )(i);          
        

    Let me simple explain, when a function created nothing happen until it called so for loop in 1st code called 5 times but not called immediately so when it called i.e after 1 second and also this is asynchronous so before this for loop finished and store value 5 in var i and finally execute setTimeout function five time and print 5,5,5,5,5

Here how it solve using IIFE i.e Immediate Invoking Function Expression

   (function(j)  //i is passed here           
                setTimeout(function()
                    console.log(j);
                ,1000);
            )(i);  //look here it called immediate that is store i=0 for 1st loop, i=1 for 2nd loop, and so on and print 0,1,2,3,4

For more, please understand execution context to understand closure.

- There is one more solution to solve this using let (ES6 feature) but under the hood above function is worked

 for(let i=0; i< 5; i++)           
                setTimeout(function()
                    console.log(i);
                ,1000);                        
            

Output: 0,1,2,3,4

【讨论】:

以上是关于深入理解 Javascript 中的闭包的主要内容,如果未能解决你的问题,请参考以下文章

深入理解javascript原型和闭包——继承

深入理解JavaScript系列(16):闭包(Closures)

深入理解JavaScript系列(16):闭包(Closures)

javascript深入理解js闭包

javascript深入理解js闭包(转)

深入理解JavaScript的闭包特性 如何给循环中的对象添加事件