JavaScript闭包中的内存泄漏风险

Eli*_*gem 26 javascript closures memory-leaks cross-browser

解决了

关于这个问题,网上有很多矛盾的信息.感谢@John,我设法解决了闭包(如下所示)不是内存泄漏的原因,即使在IE8中 - 它们并不像人们所说的那样普遍.事实上,我的代码中只发生了一次泄漏,事实证明并不难解决.

从现在开始,我对这个问题的回答是:
AFAIK是唯一一次IE8泄漏的时候,就是在全局对象上设置了事件/处理程序.(window.onload,window.onbeforeunload,...).要解决这个问题,请参阅下面的答案.


巨大的更新:

我现在已经完全迷失了......经过一段时间深入挖掘新旧文章,我至少留下了一个巨大的矛盾.尽管之一 JavaScript的大师的(道格拉斯·克罗克福德)说:

由于IE无法完成其工作并重新开始循环,因此我们不得不这样做.如果我们明确地打破循环,那么IE将能够回收内存.根据微软的说法,闭包是造成内存泄漏的原因.这当然是非常错误的,但它导致微软向程序员提供了关于如何应对微软错误的非常糟糕的建议.事实证明,很容易打破DOM端的循环.几乎不可能在JScript端打破它们.

正如@freakish所指出的,我的下面的代码段与jQuery的内部工作类似,我觉得我的解决方案不会导致内存泄漏.与此同时,我找到了这个MSDN页面,该部分Circular References with Closures对我特别感兴趣.下图几乎是我的代码如何工作的示意图,不是它:

带闭包的循环引用

唯一的区别是我的常识是不将我的事件监听器附加到元素本身.
所有相同的Douggie都是非常明确的:闭包不是IE中mem泄漏的根源.这个矛盾使我无法理解谁是对的.

我也发现泄漏问题在IE9中也没有完全解决(找不到链接ATM).

最后一件事:我还得知IE管理JScript引擎之外的DOM,当我<select>根据ajax请求更改元素的子元素时,这让我感到烦恼:

function changeSeason(e)
{
    var xhr,sendVal,targetID;
    e = e || window.event;//(IE...
    targetID = this.id.replace(/commonSourceFragment/,'commonTargetFragment');//fooHomeSelect -> barHomeSelect
    sendVal = this.options[this.selectedIndex].innerHTML.trim().substring(0,1);
    xhr = prepareAjax(false,(function(t)
    {
        return function()
        {
            reusableCallback.apply(this,[t]);
        }
    })(document.getElementById(targetID)),'/index/ajax');
    xhr({data:{newSelect:sendVal}});
}

function reusableCallback(elem)
{
    if (this.readyState === 4 && this.status === 200)
    {
        var data = JSON.parse(this.responseText);
        elem.innerHTML = '<option>' + data.theArray.join('</option><option>') + '</option>';
    }
}
Run Code Online (Sandbox Code Playgroud)

如果IE确实管理DOM就像JScript引擎不存在那样,那么使用此代码不会释放选项元素的几率是多少?
我故意添加了这个片段作为示例,因为在这种情况下,我将作为闭包范围一部分的变量作为全局函数的参数传递.我找不到关于这种做法的任何文档,但根据Miscrosoft提供的文档,它应该打破可能发生的任何循环引用,不是吗?



警告:冗长的问题......(对不起)

我已经编写了几个相当大的JavaScripts来在我的Web应用程序中进行Ajax调用.为了避免大量的回调和事件,我正在充分利用事件委托和关闭.现在我写了一个函数,让我想知道可能的内存泄漏.虽然我知道IE> 8的关闭比它的前辈好很多,但公司的政策是支持IE 8.

下面我提供了一个我正在谈论的例子,在这里你可以找到一个类似的例子,虽然它不使用ajax,但是一个setTimeout,结果几乎是一样的.(你当然可以跳过下面的代码,问题本身)

我想到的代码是这样的:

function prepareAjax(callback,method,url)
{
    method = method || 'POST';
    callback = callback || success;//a default CB, just logs/alerts the response
    url = url || getUrl();//makes default url /currentController/ajax
    var xhr = createXHRObject();//try{}catch etc...
    xhr.open(method,url,true);
    xhr.setRequestMethod('X-Requested-with','XMLHttpRequest');
    xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');
    xhr.setRequestHeader('Accept','*/*');
    xhr.onreadystatechange = function()
    {
        callback.apply(xhr);
    }
    return function(data)
    {
        //do some checks on data before sending: data.hasOwnProperty('user') etc...
        xhr.send(data);
    }
}
Run Code Online (Sandbox Code Playgroud)

所有非常简单的东西,除了onreadystatechange回调.我直接绑定处理程序时注意到IE的一些问题:xhr.onreadystatechange = callback;因此匿名函数.不知道为什么,但我发现这是最简单的方法.

正如我所说的,我正在使用大量的事件委托,所以你可以想象,访问触发ajax调用的实际元素/事件可能会有用.所以我有一些看起来像这样的事件处理程序:

function handleClick(e)
{
    var target,parent,data,i;
    e = e || window.event;
    target = e.target || e.srcElement;
    if (target.tagName.toLowerCase() !== 'input' && target.className !== 'delegateMe')
    {
        return true;
    }
    parent = target;
    while(parent.tagName.toLowerCase() !== 'tr')
    {
        parent = parent.parentNode;
    }
    data = {};
    for(i=0;i<parent.cells;i++)
    {
        data[parent.cells[i].className] = parent.cells[i].innerHTML;
    }
    //data looks something like {name:'Bar',firstName:'Foo',title:'Mr.'}
    i = prepareAjax((function(t)
    {
        return function()
        {
            if (this.readyState === 4 && this.status === 200)
            {
                //check responseText and, if ok:
                t.setAttribute('disabled','disabled');
            }
        }
    })(target));
    i(data);
}
Run Code Online (Sandbox Code Playgroud)

如您所见,onreadystatechange回调是函数的返回值,它target在调用回调时提供对元素的引用.感谢事件委托,当我决定将它从DOM中删除时,我不再需要担心可能绑定到该元素的事件(我有时会这样做).
但是,在我看来,回调函数的调用对象可能对IE的JScript引擎及其垃圾收集器来说太过分了:

Event ==> handler ==> prepareAjax是一个非常正常的调用序列,但是回调参数:

[匿名.func(参数t = target)返回anon.F(有权访问t然后返回目标)]
   ===>传递给一个匿名回调函数,使用.apply方法调用xhr对象,反过来一个私有变量到prepareAjax函数

我用FF和chrome测试了这个"结构".它在那里工作得很好,但是在关闭时关闭闭包这种关闭的callstack,在每次传递对DOM元素的引用都是IE中的一个问题(尤其是IE9之前的版本)?


不,我不打算使用jQuery或其他库.我喜欢纯粹的JS,并希望尽可能多地了解这种严重低估的语言.代码片段不是实际的复制粘贴示例,但提供IMO,很好地表示我在整个脚本中如何使用委托,闭包和回调.因此,如果某些语法不太正确,请随意纠正它,但当然,这不是这个问题的内容.

Joh*_*een 23

我曾经在非浏览器的EcmaScript(错误的...... JScr ... JavaScript)项目中使用Microsoft的JavaScript前程序管理器.我们对闭包进行了一些冗长的讨论.最后,重点是他们更难以GC,而不是不可能.我必须阅读DC关于MS如何"错误"的讨论,闭包会导致内存泄漏 - 因为在IE的旧实现中,闭包肯定是有问题的,因为它们很难用MS实现垃圾收集.我觉得奇怪的是,一个雅虎家伙会试图告诉MS架构师他们的代码已知问题在其他地方.尽管我很欣赏他的作品,但我看不出他在那里有什么基础.

请记住,您在上面引用的文章是指IE6,因为IE7在撰写本文时仍处于重大发展阶段.

作为一个旁白 - 感谢上帝,IE6已经死了(不要让我挖掘葬礼图片).虽然,不要忘记它的遗产...我还没有看到有人提出一个可靠的论点,即它在发布的第一天它不是世界上最好的浏览器 - 问题在于它们赢得了浏览器大战.因此,相当于他们历史上最大的一个错误之一 - 他们事后解雇了这支队伍,并且停滞了将近5年.IE团队多达4或5个人多年来一直在修复漏洞,造成巨大的人才流失并且大幅落后于曲线.当他们重新雇用团队并意识到他们在哪里时,他们落后了多年,因为他们处理了一个没有人真正理解的单一哥特式代码库.这是我作为公司内部人员的观点,但并不直接与该团队联系在一起.

还记得,IE从未针对闭包进行优化,因为没有ProtoypeJS(哎呀,没有Rails),而且jQuery在Resig的头脑中几乎没有一丝闪光.

在撰写本文时,他们仍然瞄准具有256兆内存的机器,这也没有使用寿命.

在让我读完你整本书的一个问题后,我认为将这一历史课提供给你是公平的.

最后,我的观点是你引用的是非常过时的材料.是的,避免IE6中的闭包,因为它们会导致内存泄漏 - 但在IE6中没有?

最后,MS已经解决并继续解决这个问题.即使在那个时候,你也会做出一定程度的关闭.

我知道他们在IE8周围的这个领域做了大量的工作(因为我的不可思议的项目使用了非实时的标准JavaScript引擎),而且这项工作一直持续到IE9/10.StatCounter(http://gs.statcounter.com/)表明,IE7的市场份额下降到1.5%,低于一年前的6%,而在开发"新"网站时,IE7的相关性越来越低.你也可以为NetScape 2.0开发,它引入了JavaScript支持,但这只会稍微不那么愚蠢.

真的......不要为了不再存在的发动机而试图过度优化.