Search code examples
javascriptobject-literal

Setting object literal property value via asynchronous callback.


I'm creating a self-contained javascript utility object that detects advanced browser features. Ideally, my object would look something like this:

Support = {
    borderRadius : false, // values returned by functions
    gradient     : false, // i am defining 
    dataURI      : true
};

My current problem deals with some code I'm adapting from Weston Ruter's site which detects dataURI support. It attempts to use javascript to create an image with a dataURI source, and uses onload/onerror callbacks to check the width/height. Since onload is asynchronous, I lose my scope and returning true/false does not assign true/false to my object.

Here is my attempt:

Support = {
    ...
    dataURI : function(prop) {
        prop = prop; // keeps in closure for callback
        var data = new Image();
        data.onload = data.onerror = function(){
            if(this.width != 1 || this.height != 1) {
                prop = false;
            }
            prop = true;
        }
        data.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
        return -1;
    }(this)
};

I'm executing the anonymous function immediately, passing this (which I hoped was a reference to Support.dataURI), but unfortunately references the window object -- so the value is always -1. I can get it to work by using an externally defined function to assign the value after the Support object is created... but I don't think it's very clean that way. Is there a way for it to be self-contained? Can the object literal's function reference the property it's assigned to?

EDIT -----------------------------------
Not exactly the answer to my question (as phrased) so I'm not going to post an additional answer, BUT... I've decided to use a singleton object instead of an object literal. Here is the working code:

Support = new function Support() {
    var that = this;
    this.dataURI = function() {
        var data = new Image();
        data.onload = data.onerror = function(){
            if(this.width != 1 || this.height != 1) {
                that.dataURI = false;
            } else {
                that.dataURI = true;
            }            
        }
        data.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
        return that.dataURI;
    }();
};

Solution

  • Can the object literal's function reference the property it's assigned to?

    Yes, it can. Every function has a local argumentsvariable, which has a callee property that references the called function. So if you wanted to reference Support.dataURI, you could simply use that:

    var Support = {
        ...
        dataURI: function(prop)
        {
            // Do whatever here with arguments.callee
        }        
    };
    

    However, I don't think that's really what you want. It looks like you're trying to reference the property name, not the value—in which case your answer is no. A JavaScript method has no way of knowing what properties it's being stored in; indeed it can be stored in multiple properties simultaneously. For example:

    var Support = {
        ...
        dataURI: function(prop)
        {
        }        
    };
    Support.somethingElse = Support.dataURI;
    

    Here, Support has two properties that both point to the same method. The method itself, however, has no way of knowing which reference was called. And since the Support object has yet to be declared, you have no way of referencing it.

    In fact (though you ask if the "object literal's function [can] reference the property it's assigned to"), the function isn't even stored on the object: its result is. So the method itself isn't associated with Support in any way.

    I'm afraid the best you can do it declare the object, then set its dataURI property.


    In Response to Your Edit

    First of all, you don't need the "new" keyword before "function". Still, I don't see how this could work in any browser. If you simplify it, lines 3-14 of your code have the form `this.dataURI = fn();'. Here's how a statement like this works:

    1. The value is determined by executing fn()
    2. The value is assigned to the dataURI property.

    At the point that fn() is being executed, dataURI hasn't yet been assigned, so there's no way to access its (correct) value to return it.

    In addition, you have an (at least potentially) asynchronous process (waiting for the image to load or error), so the value won't even have been set by the callback. Basically, you're trying to wrap an asynchronous process (onload event) in a synchronous one (function call).

    Finally, there's no need for you to create and execute a function here. Just get rid of it:

    var Support = function() {
        var that = this;
    
        var data = new Image();
        data.onload = data.onerror = function(){
            if(this.width != 1 || this.height != 1) {
                that.dataURI = false;
            } else {
                that.dataURI = true;
            }            
        }
        data.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
    
    };
    

    But remember: there's still no guarantee (that I know of, at least) that the onload and onerror handlers will fire synchronously (immediately) when you set the source—in fact, there may even be a guarantee that they won't! So you're likely going to add a callback to your Support object, and execute it once dataURI is set. Then, though, you'll have to guard against the possibility that onload or onerror is executed synchronously. Something like this should work:

    var Support = function() {
        var that = this;
    
        this.getDataURI = function()
        {
            var data = new Image();
            data.onload = data.onerror = function(){
                if(this.width != 1 || this.height != 1) {
                    that.dataURI = false;
                } else {
                    that.dataURI = true;
                }
                if (that.onDataURI)
                    that.onDataURI();
            }
            data.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
        }
    };
    
    var mySupport = new Support();
    mySupport.onDataURI = function() { alert(mySupport.dataURI); }
    mySupport.getDataURI();