ExtJs 4/Touch 2 - Ext.clone can lead to stack overflow with circular references

It feels good to be blogging again after a gap, the last couple of months or so have been way too hectic. Although I expect the schedule to remain clogged up for some more time, I will try to squeeze in some blog posts over the next couple of days. So, lets now come to the topic of this blog post, Ext.clone in ExtJs/Touch resulting in stack overflows while trying to clone an object containing circular references.

This happened to my colleague I think a couple of days ago while working with Sencha Touch Preview 2. Basically he was trying to pass config objects for component instantization to Ext.Panel.add() when he got stack overflow error and he had no idea why. After some debugging, he was able to narrow down the reason to the ns config option that we use a lot (if you have read my earlier blog post, you would know that we use a custom "ns" config option as an easy way to maintain component references and I have ported over that concept to Touch too that I am gonna blog about in an upcoming post).

So with the "ns" config option, the stack overflow occured trying to instantiate the component from its config, and without this option, it worked. When I went down in the framework to figure out the whys and hows, I noticed that the stack overflow occured in Ext.clone method calling itself recursively and infinitely. On further drill-down, I was able to locate a call to Ext.clone from Ext.Object.merge which was further called from initConfig method of Ext.Base class.

The object leading to stack overflow was the object passed to our custom "ns" config option and the reason was as simple as that object containing circular references. We faced this problem with Touch 2, but with Touch itself sharing a good quantum of its core infrastructure with ExtJs 4, I was sure the issue would exist with ExtJs 4 also and it did exist when I checked it out with ExtJs4. You can try the same in the example below, try clicking the first button which says: "Invoke method to clone an object having circular references typically with Ext.clone()".

 

 

And following is the simple test case to reproduce the above problem:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }var a = {}; var b = {}; a.b = b; b.a = a; try { var c = Ext.clone(a); //Control will never reach here because of stack overflow in above line. alert(c); } catch (e) { alert(e); }{/syntaxhighlighter}

The next step was to find a suitable solution or work-around to the issue and obviously the place to start looking out was the source of Ext.clone method. I had hoped the method would be implementing some sort of interface based cloning, where it would first check for the presence of something like clone, _clone or another suitably named method on an object, which if present would be used to clone that object. And in its absence, Ext.clone would proceed with its regular logic of manually cloning the object by recursively iterating over its properties.

But as they say, life it not perfect, and so isn't ExtJs/Touch (despite all their goodness). Ext.clone always tries to clone an object recursively leading to stack overflows for objects with circular references.

But they also say life is what you make out of it. And I saw a ray of hope in the source for Ext.clone. Please have a look at a portion of source code from this method below:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }clone: function(item) { if (item === null || item === undefined) { return item; } // DOM nodes // TODO proxy this to Ext.Element.clone to handle automatic id attribute changing // recursively if (item.nodeType && item.cloneNode) { return item.cloneNode(true); } var type = toString.call(item); // Date if (type === '[object Date]') { return new Date(item.getTime()); } //... more logic for recursive cloning{/syntaxhighlighter}

Did you notice something interesting? Did you notice that Ext.clone delegates cloning to the browser for DOM nodes if the object contained both of 'nodeType' and 'cloneNode' members in which case Ext invokes cloneNode on the object and uses its return value.

What if I add a dummy nodeType property and a custom cloneNode method on my object containing circular references. To try it out, I modified the text case to the following:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }var a = {}; var b = {}; a.b = b; b.a = a; a.nodeType = 'dummy'; a.cloneNode = function(bool) { return (this); } try { var c = Ext.clone(a); alert(c); } catch (e) { alert(e); }{/syntaxhighlighter}

As as was to be expected, it worked prfectly this time. Notice I added a dummy nodeType property to the object being cloned and a cloneNode method which returned the same object itself. It was fine in my case, but for your objects containing such circular referecnes, you might want to provide a more meaningful logic to clone the object (and not simply return the original object).

You can click the second button on the above demo (the one saying "Invoke method to clone an object having circular references and custom cloneNode method with Ext.clone()") and you would notice that it works fine now with these adjustments to the object containing circular references.

I have created a thread for this issue on Sencha Forums here, and you might want to go there and give your opinion on how this issue should be addressed (should Ext.clone delegate cloning via a custom method on the object if present, or should it try itself like print_r in php does without going into infinite recursion, or if you have some other suggestion).

You can find the source for the test-cases attached with this blog post below.

 

AttachmentSize
HTML icon ext-clone-stack-overflow.htm1.96 KB

Comments

Hi,

I am trying to develop a HTML 5 app, and today I encountered Uncaught RangeError: Maximum call stack size exceeded, Ext.apply.clone.

I am tring to generate a list using Navigation view and when I am trying to push a details view ontap, I get this error because I have added data:record.data in the push function.

If I remove this statement, I don't get this error. But the page is empty as I cannot get the data.

Can you suggest any fix to this ?

Thanks.

rahul's picture

Hi Abhishek, you might want to show data in the view in its "painted" or "activate" event.