So I spent an entire day discovering a quirk about javascript that I must now share. In a previous post on creating default settings for classes/objects I discussed the following technique:

JavaScript:
var Widget = new Class({
    initialize: function(element, options){
        this.element = element;
        this.options = Object.extend({
            offsetX: 0,
            offsetY: 0
        }, options || {});
        this.setPosition();
    },
    setPosition: function(){
        this.element.setStyles({
            left: this.options.offsetX + 'px',
            top: this.options.offsetY + 'px'
        });
    }
});

drag to resize


Now, this isn't a very useful class, but it illustrates the technique. The functions in our class don't have to worry if the options are defined; they are either what the default value is or they are what the user passed in. If the user elects to just pass in a subset of the values, that's fine:

JavaScript:
var myWidget = new Widget(myElement, {offsetX: 100});
//myElement will be offset by 100 on the left,
//zero (the default) on the top

drag to resize


But what if you want to extend the functionality of your class later? What if you want to be able to insert more default options?

Here's what I was doing that caused me trouble:

JavaScript:
var Widget = new Class({
    initialize: function(element, options){
        this.element = element;
        this.setOptions(options);
        this.setPosition();
    },
    defaultOptions: {
        offsetX: 0,
        offsetY: 0
    },
    setOptions: function(options){
        this.options = Object.extend(this.defaultOptions, options || {});
    },
    setPosition: function(){
        this.element.setStyles({
            left: this.options.offsetX + 'px',
            top: this.options.offsetY + 'px'
        });
    }
});

drag to resize


Then, if I wanted to extend it:

JavaScript:
var BetterWidget = Widget.extend({
    initialize: function(element, options){
        //add some new defaults
        options = Object.extend({
            marginTop: 0,
            marginBottom: 0
        }, options || {});
        this.parent(element, options);
    },
    setPosition: function(){
        this.parent();
        this.element.setStyles({
            marginTop: this.options.marginTop + 'px',
            marginBottom: this.options.marginTop + 'px'
        });
    }
});

drag to resize


In theory, this should work just fine. But the problem is that .defaultOptions is an object, and in javascript, whenever you say one object = another object, the two are linked together, and changes to one make changes to the other.

The result is that when you create an instance of BetterWidget:

JavaScript:
var myBetterWidget = new BetterWidget(myElement, {offsetX: 100, marginTop: 100, marginBottom: 100});

drag to resize


You'll create your instance as you'd expect, but this section in Widget:

JavaScript:
setOptions: function(options){
    this.options = Object.extend(this.defaultOptions, options || {});
},

drag to resize


Will actually end up changing the prototype of defaultOptions, thereby setting every instance of Widget (and BetterWidget) to those values. So every time you create a new instance, every instance will get the settings you specify.

This is no good for obvious reasons. Here's the solution:

JavaScript:
var Widget = new Class({
    initialize: function(element, options){
        this.element = element;
        this.setOptions(options);
        this.setPosition();
    },
    getDefaultOptions: function(options) {
        return {
            offsetX: 0,
            offsetY: 0
        }
    },
    setOptions: function(options){
        this.options = Object.extend(this.getDefaultOptions(), options || {});
    },
    setPosition: function(){
        this.element.setStyles({
            left: this.options.offsetX + 'px',
            top: this.options.offsetY + 'px'
        });
    }
});

//and then to extend the widget:
var BetterWidget = Widget.extend({
    getDefaultOptions: function() {
        //merge these defaults with the defaults
        //from the parent class
        return Object.extend({
            offsetX: 0,
            offsetY: 0
        }, this.parent() || {});
    },
    setPosition: function(){
        this.parent();
        this.element.setStyles({
            marginTop: this.options.marginTop + 'px',
            marginBottom: this.options.marginTop + 'px'
        });
    }
});

drag to resize


Because we return an object, instead of defining one in the class itself, there's no link to the Class in the default options object, and we don't run into this inheritance problem.

As I said, I spent a day figuring out that the options were my problem, but even then I couldn't figure out why. My pal Valerio (who is the author of Mootools) showed me the problem when I got him to look at my code. He'd had the same problem in developing "mooglets" for Mootools and had spent a solid day or two figuring out the problem.