(2 minute read)
Javascript has given you the ability to define properties on objects for a while now, utilising Object.defineProperty to control how properties are read and written. In this post I will detail how to make getters and setters which can be overridden in subclasses. At the end I will provide a utility method you can add to your own code which makes it real easy to add overridable getters and setters to your classes.
First, let's take a quick look at how 'properties' work. Let's say we define a class as follows:
var ClassA = function() {}
Object.defineProperty(ClassA.prototype, 'flag', {
get: function() {
return '['
this._flag ']';
},
set: function(val) {
this._flag = val;
}
});
So we've defined a property flag in ClassA. Now if we do the following:
var a = new ClassA();
a.flag = 'test';
console.log(a.flag); // [test]
console.log(a._flag); // test
Notice that when we call a.flag it executes the method assigned as the getter, which in turn returns the set value surrounded by square brackets. Whereas if we directly access _flag we get the original value we put in. Technically speaking, there's no restriction to what the getter and setter methods can do. We didn't have to name the internal member variable _flag - we could have named it anything. We could have modified an entirely different member variable during the setter. The point is that clients of the API only need to be told about flag whereas internally we are free to choose exactly where we store the data.
Now let's say we wish to subclass our ClassA and override the getter for the property flag. How can we override the getter? Well, we have to rewrite ClassA slightly to make this possible:
var ClassA = function() {}
Object.defineProperty(ClassA.prototype, 'flag', {
get: function() {
return this.__get_flag();
},
set: function(val) {
this._flag = val;
}
});
ClassA.prototype.__get_flag = function() {
return '[' + this._flag + ']';
};
By writing the getter as just another method available on ClassA.prototype we make it overridable. Now we can write the subclass:
var ClassB = function() {}
ClassB.prototype = Object.create(ClassA.prototype); // inheritance
ClassB.prototype.__get_flag = function() {
return '(' + this._flag + ')';
};
Now let's test it and see if we get what we want:
var a = new ClassA();
a.flag = 'test';
console.log(a.flag); // [test]
var b = new ClassB();
b.flag = 'test';
console.log(b.flag); // (test)
Note that by placing the setter method on the prototype we would be able to make that overridable too.
The following utility method makes the task of adding overridable getters and setters real easy:
Function.prototype.generateProperty = function(name, options) {
// internal member variable name
var privateName = '__'
name;
options = options || {};
options.get = ('undefined' === typeof options.get ? true : options.get);
options.set = ('undefined' === typeof options.set ? true : options.set);
// pre-initialise the internal variable?
if (options.defaultValue) {
this.prototype[privateName] = options.defaultValue;
}
var definePropOptions = {},
getterName = '__get_' + name,
setterName = '__set_' + name;
// generate the getter
if (true === options.get) {
this.prototype[getterName] = function() {
return this[privateName];
};
}
// use custom getter
else if (options.get) {
this.prototype[getterName] = options.get;
}
// disable getter
else {
this.prototype[getterName] = function() {
throw new Error('Cannot get: ' + name);
}
}
definePropOptions.get = function() {
return this[getterName].call(this);
};
// generate the setter
if (true === options.set) {
this.prototype[setterName] = function(val) {
this[privateName] = val;
};
}
// use custom setter
else if (options.set) {
this.prototype[setterName] = options.set;
}
// disable setter
else {
this.prototype[setterName] = function() {
throw new Error('Cannot set: ' + name)
};
}
definePropOptions.set = function(val) {
this[setterName].call(this, val);
};
// do it!
Object.defineProperty(this.prototype, name, definePropOptions);
};
It gives you a large amount of flexibility:
1. You can choose to generate both a getter and a setter, just a getter on its own, just a setter on its own, or even neither (that would be a strange choice though!). 2. You can supply a custom getter and setter or have it generate default ones for you. 3. It sets the internal member variable (which backs the property) to also be on the prototype, making it easy to override its value in subclasses. It can even initialise it to a default value if you provide one. 3. The method itself is attached to the Function object, making for ease of use and readable code.
Here is an example of its use:
var ClassA = function() {}
ClassA.generateProperty('name', {
defaultValue: 'john',
get: function() {
return this.__name + ' smith';
},
set: false
});
var a = new ClassA();
console.log(a.name); // john smith
a.name = 'terry'; // Error: cannot set: name
console.log(a.name); // john smith
a.__name = 'mark';
console.log(a.name); // mark smith
Now we can override the getter in a subclass:
var ClassB = function() {}
ClassB.prototype = Object.create(ClassA.prototype);
ClassB.prototype.__get_name = function() {
return this.__name + ' oliver';
}
ClassB.prototype.__set_name = function(val) {
this.__name = val;
}
var b = new ClassB();
console.log(b.name); // john oliver
b.name = 'terry';
console.log(b.name); // terry oliver
b.__name = 'mark';
console.log(b.name); // mark oliver
It seems a bit inelegant to have to know the method name of the getter and/or setter in order to override it. We could add more utility methods - e.g. overrideGetter and overrideSetter - to take care of the gory details if we wanted to.
Meanwhile you can also see the above utility method as a Gist: https://gist.github.com/hiddentao/5946053.