DOM:为什么这是一个内存泄漏?

30
请考虑来自Mozilla JavaScript内存泄漏文档的这段引用:
function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
}

The above code sets up the element to turn red when it is clicked. It also creates a memory leak. Why? Because the reference to el is inadvertently caught in the closure created for the anonymous inner function. This creates a circular reference between a JavaScript object (the function) and a native object (el).

请简明扼要地解释上述泄漏原因,我没有理解到关键点。
由于泄漏,网站/页面是否面临安全问题?我该如何避免它们?还有哪些代码可能会导致内存泄漏?如何判断内存泄漏已经发生?
我是内存泄漏的绝对初学者。有人能够逐步为我澄清这个问题吗?同时,有人能帮助我澄清这句话“这在JavaScript对象(函数)和本地对象(el)之间创建了循环引用。”吗?

1
http://www.javascriptkit.com/javatutors/closuresleak/, http://www.google.com/search?q=explanation+of+javascript+memory+leaks - CBroe
1
@GrantKiely 这是来自 MDN 的内容。 - Maizere Pathak.Nepal
4
@ Maizere:你引用的地方(MDN),我现在已经在问题正文中添加了链接,非常清楚地解释了这个特定的内存泄漏情况。你是否有具体不理解的问题,或者想深入了解闭包? - Crescent Fresh
6
顺便说一下,我想强调这是一个仅限于微软的漏洞。在你的代码中放置这样的循环引用会进一步降低IE浏览器的体验,因此鼓励用户在每个机会下切换到更好、更安全的浏览器。请注意避免这样做。 - Michael Lorton
1
@CrescentFresh,我不理解这个语句:“因为对el的引用无意中被捕获在为匿名内部函数创建的闭包中。这会在JavaScript对象(函数)和本地对象(el)之间创建一个循环引用。”我的英语不太好,所以需要一个简单明了的解释。 - Maizere Pathak.Nepal
显示剩余8条评论
4个回答

20

有两个概念可以帮助您理解这个例子。

1)闭包

闭包的定义是:每个内部函数都可以访问其父函数的变量和参数。

addHandler()函数完成时,匿名函数仍然可以访问其父级变量el

2)函数=内存

每次定义一个function都会创建一个新对象。 这个例子稍微有些令人困惑,因为onclick是一个只能设置一次DOM元素的事件。

那么el.onclick = function(){};肯定会覆盖旧函数,对吗?

错了!每次运行addHandler时,都会创建一个新的函数对象。

总之:

每次函数运行时,它都会创建一个新的对象,其中包含一个闭包,其中包含el。由于匿名函数保持对el的访问,垃圾回收器无法将其从内存中删除。

匿名函数将保持对el的访问,而el将具有对函数的访问,这是循环引用,在IE中会导致内存泄漏。


9
在这种情况下,“this”是指代“el”。没错,但这个特定的事实与闭包无关。无论它们是否是闭包,这都是事件处理程序的一个属性。内部函数可以访问“el”这一事实很重要。 - Felix Kling
@FelixKling 谢谢!我从我的答案中删除了那部分。 - gkiely
2
只是出于好奇,如果从DOM中删除元素,函数是否会超出范围,还是由于循环引用两者仍然存在? - Orangepill
你的第二点对我来说似乎不正确。在Firefox 65中,onclick方法的速记绝对覆盖了先前的值,因此先前的订阅本质上被销毁了。 - James Wright

9
在JavaScript中定义函数时会创建一个执行上下文,该执行上下文包含对作用域链中所有变量的引用,从全局作用域一直到本地作用域。
function test()
{
    var el = document.getElementById('el');
    el.onclick = function() {
        // execution context of this function: el, test
        alert('hello world');
    }
}

test()完成后,匿名函数尚未被回收,因为它现在被分配给DOM的一个元素; 即,它被DOM元素的属性所引用。
同时,DOM元素本身也是函数执行上下文的一部分,由于循环引用,现在无法回收,即使实际上并没有立即使用; 您可以在this answer中找到演示。
话虽如此,现在大多数JavaScript引擎(甚至在IE中发现的引擎)都使用更先进的垃圾回收器,可以更好地识别未使用的变量,使用标记和清除或代际/短暂垃圾回收等技术。
为了确保您不会在任何浏览器上遇到问题(尽管由于页面的典型寿命,这基本上是理论上的):
document.getElementById('el').onclick = function() {
    alert('hello world');
}

无法获取此内容:“但是el也没有被回收,因为它是该函数执行上下文的一部分。” - Maizere Pathak.Nepal
1
@Maizere 执行上下文包含(或者说引用了)el,因此由于引用计数的原因它无法被回收利用。 - Ja͢ck
1
是的,相反地,执行 document.getElementById('el').onclick = function() { ... } 不会导致内存泄漏。 - Ja͢ck
啊,我需要休息。谢谢你的帖子。 - Maizere Pathak.Nepal
最近垃圾收集器的进展使这个问题过时了。我注意到你进行了接受/取消接受操作,所以我决定在我的答案中添加这一部分 :) - Ja͢ck

2
另外请参阅MS有关此问题的更多信息部分:

这个内存泄漏是由于DOM对象是非JScript对象。DOM对象不在JScript的标记和扫描垃圾回收机制中。因此,DOM对象和JScript处理程序之间的循环引用直到浏览器完全关闭页面时才会被打破。

但需要注意的是,与该文章所述相反(当浏览器转到新页面时将回收内存),此文章确认IE 6中的一个错误导致内存永久泄漏。

1
JavaScript的内存管理通常是这样工作的:“只要可以访问,就保留它”。这基本上是任何垃圾收集驱动的内存模型背后的范例。
垃圾收集器倾向于非常擅长它们所做的事情,它们甚至可以检测到某个元素组只在该元素组内部可达。这些组也被称为循环引用,因为如果您跟随引用,您会最终到达一个您已经访问过的元素:您已经形成了一个圆。
然而,在您的示例中,您实际上有来自两个不同“世界”的两个对象:

Circular references

Internet Explorer使用自己的垃圾回收机制,与JavaScript使用的机制是分开的。两者之间的交互可能会导致内存泄漏。这正是发生内存泄漏的原因。

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