Julian Jelfs’ Blog

Scope shenanigans part three

Posted in Javascript by julianjelfs on January 24, 2010

So in my previous posts here and here I described a problem I was having with Microsoft Ajax script library serialisation of Dates that had been created in a different scope to that where the serialisation is actually taking place. I also described a somewhat hacky solution that involves iterating through the data to be serialised, tagging dates and then switching scope and recreating the dates in the new scope.

This works fine, but I don’t like the idea of having to recreate all these dates all the time so I go to thinking a bit more about what (someDate instanceof Date) actually means. Roughly it means, was this date created with the Date function. Given that I wandered if the following two statements were equivalent:

var date = new Date();
alert(date instanceof Date);    //true
alert(date.constructor === Date);    //true

If so then perhaps I can do the following:

var dateA = top.frames["frameB"].date;
alert(dateA instanceof Date);    //false
dateA.constructor = Date;    //change the constructor function
alert(dateA instanceof Date);    //true

If so, I can avoid having to recreate the Date objects, I can just reassign their constructor functions. Sadly, it doesn’t work. It seems that there is more to instanceof than just checking the constructor function. So we still have to be wary of using instanceof, but remember that the original problem was with the Microsoft Ajax script serialisation so all is not lost. The MS code detects type like this:

Type.prototype.isInstanceOfType = function Type$isInstanceOfType(instance) {    
    var e = Function._validateParams(arguments, [{name: "instance", mayBeNull: true}]);    
    if (e) throw e;    
    if (typeof(instance) === "undefined" || instance === null) return false;    
    if (instance instanceof this) return true;    
    var instanceType = Object.getType(instance);    
    return !!(instanceType === this) ||           
        (instanceType.inheritsFrom && instanceType.inheritsFrom(this)) ||           
        (instanceType.implementsInterface && instanceType.implementsInterface(this));
}

So it uses instanceof first, but if that returns false, it goes on to call Object.getType which looks like this:

Object.getType = function Object$getType(instance) {    
    var e = Function._validateParams(arguments, [{name: "instance"}]);    
    if (e) throw e;    
    var ctor = instance.constructor;    
    if (!ctor || (typeof(ctor) !== "function") || !ctor.__typeName || (ctor.__typeName === 'Object')) {        
        return Object;    
    }    
    return ctor;
}

Which is basically just returning the object’s constructor property. So by overwriting the constructor argument as described, although instanceof still cannot be trusted, Date.isInstanceOfType(object) as defined in the Microsoft Ajax script library will work as expected and therefore so will MS javascript serialisation.

The upshot is that by overwriting the date object’s constructor property with the Date function from the required scope we can get Date.isInstanceOfType(object) to return true and there is no need to completely recreate the date in the required scope. If, however, you want instanceof to work – the only way I can achieve that is to recreate the date. If anyone knows of a better way, I’d love to hear it.

Advertisements

Solution to the cross-frame instanceof scope problem

Posted in Javascript by julianjelfs on January 15, 2010

In my previous post I explained how I ran into the problem that dateB defined on frameB would return false if you execute dateB instanceof Date in frameA. I thought that this was because the data somehow got mangled when marshalled from one window to another. In fact it is nothing to do with that. The instanceof function is working entirely correctly, it’s just maybe not doing what you expect.

When you write (dateB instanceof Date) you are really asking was dateB created with function Date. Now when you add the implicit scope you begin to see why it doesn’t work the way you want. dateB was created in frameB with the function frameB.Date. So when you say dateB instanceOf frameA.Date as you implicitly are when you run this code in frameA, then of course it should and does return false.

If you explicitly call dateB instanceof frameB.Date it still returns true.

If you bear this in mind when calling functions that return data on other frames you can get round it. One approach which I have taken is shown here:

var frameB = top.frames["frameB"];

var dateB = crossFrame(frameB, frameB.getMeADate);

alert(dateB instanceof Date);     //give true

function crossFrame(target, func, parms){
    var result = func.apply(target, parms);
    result = tagDates.call(target, result);
    tagDates.call(target, parms);
    fixDates(parms);
    return fixDates(result);
}
function fixDates(val){
    if (val == null) return val;
    if (val["_date"]!==undefined) {        
        return new Date(val);
    }
    if (typeof (val) != 'object') return val;
    if (val instanceof Array) {
        for (var j = 0,len=val.length; j < len; j++) {
            val[j] = fixDates(val[j]);
        };
        return val;
    }
    for (var prop in val)
        val[prop] = fixDates(val[prop]);
    return val;
}

function tagDates(val){
    if (val instanceof Date) {
        val._date = true;
        return val;
    }
    if (typeof (val) != 'object') return val;
    if (val == null) return val;
    if (val instanceof Array) {
        for (var j = 0,len=val.length; j < len; j++) {
            val[j] = tagDates(val[j]);
        };
        return val;
    }
    for (var prop in val)
        val[prop] = tagDates(val[prop]);
    return val;
}

So we pass the target frame and the target function and the arguments array to the function crossFrame. Inside crossFrame we call the function on the target frame and get the results. At this point we are in calling frame scope. We then call tagDates using the target frame as scope. In this scope we can correctly identify the dates in the data. For each one we find, add an attribute so that we can identify it as a date later.

Then back in the calling scope we call fixDates which looks for the objects we tagged as dates and overwrites them with an equivalent Date created in the calling scope.

This means that when the data is returned from the call to crossFrame, any dates will have been re-created in the calling scope and any call to instanceof will work as expected.

The code above is not complete because it only handles dates. That’s all I have a problem with so it works for me but it could easily be extended for any other type because the underlying problem is not about types it is about scope.

Cross-frame dates cannot be serialised by Sys.Serialization.JavaScriptSerializer

Posted in Javascript by julianjelfs on January 15, 2010

Lets say you have a page with two iframes on it (frame A and frame B). In frame A we do this (forget why):

window.testDate = new Date();

and then in frame B we do this:

Sys.Serialization.JavaScriptSerializer.serialize(top.frames["frameA"].testDate);

We will get a stack overflow (in IE7 at least) when using the debug version of the Microsoft Ajax framework and we’ll just get nothing with the release version.

The root cause seems to be that in frame A testDate instanceof Date is true. But if we reference it from frame B it is false. This means that the serialiser does not correctly identify it as a date etc etc.

This is annoying if you are using the serialiser directly, but it is really annoying if you are forced to use it because you are using an MS Ajax script service proxy. I guess I will have to use jQuery to call the script service directly and take charge of serialisation myself using some other JSON library. I have seen it done but I just know it’s going to be painful…

PS – this issue is nicely described here