分享

Leak Free Javascript Closures

 weicat 2006-03-23

Leak Free Javascript Closures

October 20, 2005

Javascript closures can be a powerful programming technique. Unfortunately in Internet Explorer they are a common source of memory leaks. Therefore I propose a method to create closures that don‘t leak memory.

Problem

First start with a short explanation of the problem I tried to fix. Here is an example of a simple event handler (IE only for clarity):

function attach()
{
var element = document.getElementById("my-element");
element.attachEvent("onclick", function(){ alert("Clicked: " + element.innerHTML); });
}

This seems harmless enough, but the function (closure) is created in a scope which contains element. Since we attach the function to element, a circular reference is created and IE no longer can garbage collect element. This can easily be demonstrated by adding a large string to element.

There are a lot of solutions for this problem, of which most focus on event attaching. But this problem can also occur when Javascript objects are set as a property on an HTML element.

Solution

So we need a function that can access an HTML element without creating an inline closure that leaks memory.

The following code adds a closure method to each function. closure wraps the original function in such a way that this is set to the given object.

Function.prototype.closure = function(obj)
{
// Init object storage.
if (!window.__objs)
window.__objs = [];
// Init closure storage.
if (!this.__closureFuncs)
this.__closureFuncs = [];
// Make sure the object has an id and is stored in the object store.
var objId = obj.__closureObjId;
if (!objId)
__objs[objId = obj.__closureObjId = __objs.length] = obj;
// See if we previously created a closure for this object/function pair.
var closureFunc = this.__closureFuncs[objId];
if (closureFunc)
return closureFunc;
// Clear reference to keep the object out of the closure scope.
obj = null;
// Create the closure, store in cache and return result.
var me = this;
return this.__closureFuncs[objId] = function()
{
return me.apply(__objs[objId], arguments);
};
};

So now we can do:

function attach()
{
var element = document.getElementById("my-element");
element.attachEvent("onclick", clickHandler.closure(element));
}
function clickHandler()
{
alert("Clicked: " + this.innerHTML);
}

Which doesn‘t leak. And can also be used to run any function in a given context:

function myObject()
{
this.status = "waiting";
setTimeout(this.delayedCode.closure(this), 1000);
}
myObject.prototype =
{
delayedCode: function()
{
this.status = "done waiting";
}
};
var o = new myObject();

Some might argue that this fixes one leak with another since all closure context objects are stored in an array. Though this array will be freed on reload, it will stay in memory as long as the user stays on the page.

A simulation of a highly dynamic webpage shows that this isn‘t a big problem in practise. This shows that an html element takes about 1KB and even an application like Xopus doesn‘t create more than 10000 elements in a single session. And even if it would, it would only take about 10MB which I think is acceptable.

Update: new version with less prerequisites

The above mentioned closure function will only work if the original function does not have a (indirect) reference to the object to which the closure is attached. So this will still leak:

function attach()
{
function clickHandler()
{
alert("Clicked: " + this.innerHTML);
}
var element = document.getElementById("my-element");
element.attachEvent("onclick", clickHandler.closure(element));
}

This is caused by the fact that the created closure function still has a reference to it‘s original function (me). A new version of the closure function fixes that problem:

Function.prototype.closure = function(obj)
{
// Init object storage.
if (!window.__objs)
{
window.__objs = [];
window.__funs = [];
}
// For symmetry and clarity.
var fun = this;
// Make sure the object has an id and is stored in the object store.
var objId = obj.__objId;
if (!objId)
__objs[objId = obj.__objId = __objs.length] = obj;
// Make sure the function has an id and is stored in the function store.
var funId = fun.__funId;
if (!funId)
__funs[funId = fun.__funId = __funs.length] = fun;
// Init closure storage.
if (!obj.__closures)
obj.__closures = [];
// See if we previously created a closure for this object/function pair.
var closure = obj.__closures[funId];
if (closure)
return closure;
// Clear references to keep them out of the closure scope.
obj = null;
fun = null;
// Create the closure, store in cache and return result.
return __objs[objId].__closures[funId] = function ()
{
return __funs[funId].apply(__objs[objId], arguments);
};
};

We can now use the common pattern of creating event handlers inline:

function attach()
{
var element = document.getElementById("my-element");
element.attachEvent("onclick", function()
{
alert("Clicked: " + this.innerHTML);
}.closure(element));
}

So now we have truly leak free closures.

In addition we can also easily remove an object from the global array. The following code allows the garbage collector to free an object if there are no other references to it:

window.__objs[obj.__objId] = null;

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多