Beware: object = object has a pitfall
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:
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'
});
}
});

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:
//myElement will be offset by 100 on the left,
//zero (the default) on the top

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:
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'
});
}
});

Then, if I wanted to extend it:
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'
});
}
});

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:

You'll create your instance as you'd expect, but this section in Widget:
this.options = Object.extend(this.defaultOptions, options || {});
},

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:
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'
});
}
});

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.
Leave a Reply
You must be logged in to post a comment.