[UPDATE] I have updated this with information gleaned from http://jibbering.com/faq/faq_notes/closures.html on the true nature of closures and performed an experiment. Its worse than I thought. The ‘shotgun’ nature of scope closure in JavaScript means that its very easy to write leaky code. One of the problems with JavaScript is that you can make an effective closure over objects that are not explicitly obvious if you look at the code within the closure itself.
The very general rule of thumb seems to be The deeper you are in scope when you create your closures, the more likely you are to have leakage.
Updates are in italics.[/UPDATE]
Why does IE leak when closures are used in event listeners?
- When a closure causes a circular reference.
- A blind spot in the IE garbage collector for circular references involving DOM elements. IE uses uses “mark and sweep” for JavaScript objects, but it has a blind spot in that it uses COM “reference counting” for DOM objects.
Now, when I say closure, I don’t just mean an anonymous function. I mean a real closure, although the way that JavaScript handles scope closure this becomes effectively a moot point.
The following is not a closure, because it does not reference any variable out of scope of the function.
function(){var x = 0; return x;}
The following is a closure in the traditional sense, because the function references l_a, which is declared outside the scope of the function.
var l_a = 1; var l_f = function(){var x = l_a; return x;}
In JavaScript the following is effectively a closure too, because when you declare a function in JavaScript it gets a reference to the scope that it was declared in, and that scope contains references to all the objects that are declared in that scope. Even if you didn’t intend to maintain a reference to l_a from the closure, you effectively are.
var l_a = 1; var l_f = function(){var x = 0; return x;}
Now here is an important bit, every closure that contains an intended or unintended out of scope reference to the same variable in the same scope will be referencing the same object. And any change to that referenced object will be visible to all closures.
So…
var l_a = 1; var l_f1 = function(){return l_a;} l_a = 2; var l_f2 = function(){return l_a;} alert(l_f1()); alert(l_f2());
Both l_f1() and l_f2() will return 2 [TEST] because they both reference l_a in the same scope.
Closures declared in the same scope referencing the same variable.
This is because all closures declared in the same scope maintain a reference to the same scope, so any changes you make to any variables declared in that scope or any other sub-scope are visible to the closure.
In the case of a memory leak pattern, if these closures were event handlers, then your leak would include l_a and because of scope closure l_f1 and l_f2
But a benign looking improvement to this code can cause many more leaks and change the behavior of the code.
function MakeClosure(p_a) { return function(){return p_a;} } var l_a = 1; var l_f1 = MakeClosure(l_a); l_a = 2; var l_f2 = MakeClosure(l_a); l_a = 3; alert(l_f1()); alert(l_f2());
In this case, l_f1() will return 1.. but l_f2() will return 2 [TEST]
Why?
l_a is a scalar value, it is being copied onto the stack in each scope that is created when you call MakeClosure, so p_a is not a reference to the original l_a, it is a copy of the value of l_a at the point of which MakeClosure() is called in two different scopes.
Closures declared in different scopes referencing different copies of a variable
Now here is another important bit..
If the above pattern was involved in a leak pattern, you would leak one copy of p_a for every time MakeClosure() is called because the scope of MakeClosure(), which includes the declaration of p_a is referenced by the closure.
This does not apply to object references. See the following.
function MakeClosure(p_o) { return function(){return p_o.m_a;} } var l_o = new Object(); l_o.m_a = 1; var l_f1 = MakeClosure(l_o); l_o.m_a = 2; var l_f2 = MakeClosure(l_o); l_o.m_a = 3; alert(l_f1()); alert(l_f2());
In this case both l_f1 and l_f2 return 3 – because p_o is a reference pointer to the original l_o. [TEST]
Closures declared in different scopes using different references to the same variable
So if the above pattern was involved in a leak pattern you would leak three objects, l_o, l_f1 and l_f1 no matter how many times you call MakeClosure();
So why does the following code leak so badly? [TEST]
window.onload = init; function init() { // Start timing var T1 = (new Date()).getTime(); createLinks(); var x = document.getElementsByTagName('a'); for (var i=0;i<x.length;i++) { assignListener(x[i]); } // How long did that take? alert("it took " + ((new Date()).getTime() - T1) / 1000 + "seconds to render thisrnHit F5 and see "+ "what happens to the render time."); } function assignListener(p_e) { p_e.onclick = function () { this.firstChild.nodeValue = 'CLICK'; } }
Every time we call assignListener(p_e) we are creating a new scope that contains a new and unique reference, so we are leaking multiple scopes and because each p_e is a references to a different DOM object, it comes under the garbage collectors blind spot.
The following leak pattern is established for every element reference passed as a parameter to assignedListener.
Every time we call assignListener() we create a new scope that contains a reference to a DOM element.
If we were to be sensible and use ‘this’ instead of p_e, then we would avoid the issue all together.
The deeper you are in scope when you create your closures, the more likely you are to have leakage.
But – these leaks are very subtle. Its not always obvious that they are going to occur, even when we do something innocent looking. One of the problems with JavaScript is that you can make an effective closure over objects that are not implicitly obvious if you look at the code in the closure itself.
There are four possible solutions.
- Ban closures as event listeners – but they are very useful and they are just one part of the IE leakage issue.
- Force programmers to go through a complicated framework that has no leak patterns. Lets face it, You can’t force a programmer to do anything and its not going to stop them from accidentally writing leaky code.
- Wait for IE7 and hope it does not leak.
- Write your own garbage collector. This is what I did in my solution, but a very focused one. Any element that gets a listener assigned gets very carefully scrubbed after I am finished with it. Any element that is attached to a structure but is not part of the DOM is ‘garbage collected’
Full points to anyone who gets the twisted ‘Cabs’ reference in the name of this topic. 🙂
[UPDATE]
Here is my updated element scrubber.
/*! @fn MetaWrap.Page.Element.scrub = function(p_element) @param p_element A reference to the element we want to scrub @return void @brief Nulls element references that commonly cause circular references. @author James Mc Parlane @date 23 October 2005 Called for every Element that is garbage collected by MetaWrap.Page.garbageCollectListeners() or by the teardown code MetaWrap.Page.deleteListeners() As a generic leak pattern cleanup, this solution is only effective for elements have had a listener added via MetaWrap.Page.Element.addEventListener Works by nulling event listeners and custom expanded members. This will only work if you use the following naming standard for your custom expansions of the element object. m_XXX eg. m_flipover $XXX eg. $flipover */ MetaWrap.Page.Element.scrub = function(p_element) { // For each member of the element for(var l_member in p_element) { // If its an event listener or one of my user assigned // members m_XXX, $XXX (self imposed naming standard) if ((l_member.indexOf("on") == 0) || (l_member.indexOf("m_") == 0) || (l_member.indexOf("$") == 0)) { // Break the potential circular reference p_element[l_member] = null; } } }
. . You "GC" seems to only work when you assign the event using the old DOM0 model, not when you do it via "attachEvent" or "addEventListener". That makes it useless in most of modern coding.