Every now and then, I come across a coding pattern that’s more hack than it is design. One that I despise the most is the use of pseudo read-only properties in JavaScript.
At some point in your coding career, you’ve probably seen a method masquerading as a property. I’m talking about things like [cci]object.get_property()[/cci]. It works, but it’s a method call, not a real property.
The code to set up an object like this would probably look something like:
/**
* Represent a used car.
*
* @constructor
*/
function Car() {
var SELF = this,
odometer = 0;
/**
* Increment the odometer
*
* @param {Number} miles
*/
SELF.drive = function( miles ) {
odometer += Math.abs( miles );
};
/**
* Get a protected odometer reading
*
* @return {Number}
*/
SELF.get_odometer = function() {
return odometer;
};
}
var subaru = new Car();
subaru.drive( 500 );
subaru.get_odometer(); // 500
Again, it works, but it’s a hack. It’s ugly code. It’s unintuitive. It has no place in a well-structured application.
The Problem
Code should be somewhat self-documenting. Object methods and properties should be clearly named in such a way that developers will immediately know what the method does and the object contains. When methods are used as accessors for properties, objects become that much more cryptic to the next developer that comes along.
Functions like [cci]get_data()[/cci] make sense – it’s a function that retrieves (and possibly returns) information. On an object, methods like [cci]object.save_data()[/cci] make sense – it’s a function that saves (possibly to an external data store) some piece of information.
Properties like [cci]object.name[/cci] are static things. They’re properties of the object, not functionality that should be invoked. At times, though, properties need to be read-only (i.e. exposing the status of a database connection) and that can be a confusing scenario in vanilla JavaScript. The easiest way is to use the hack above to hide the variable itself within a closure and make it read-only through an accessor method.
But this method begins to look like the data manipulation methods referenced above, forcing new developers to look deeper at the code to figure out what’s going on. Making the person who has to support your code think is a bad idea; particularly if they expect your code to “just work.”
The Solution
Thanks to ECMAScript 5, you can now programatically define the properties of your objects. This means you can define a property that has a getter – allowing you to read [cci]object.property[/cci] directly – but that lacks a setter – triggering an error when attempting to set [cci]object.property[/cci].
The code below is the same as before, but refactored to expose a read-only property on our object:
/**
* Represent a used car.
*
* @constructor
*/
function Car() {
var SELF = this,
_odometer = 0;
/**
* Increment the odometer
*
* @param {Number} miles
*/
SELF.drive = function( miles ) {
_odometer += Math.abs( miles );
};
/**
* Get a protected odometer reading
*
* @return {Number}
*/
Object.defineProperties( this, {
"odometer": {
"get": function() { return _odometer; }
}
} );
}
var subaru = new Car();
subaru.drive( 500 );
subaru.odometer; // 500
subaru.odometer = 0; // does nothing
subaru.odometer; // 500
Other Uses
Another edge case that defined getters and setters satisfies is when setting a property must update an object’s internal state. This is a rare use case and obscures the functionality of your application. Be very careful using anything that looks like the code below:
/**
* Represent an elementary particle.
*
* @constructor
*/
function Fermion() {
var SELF = this,
_position = [100, 200],
_speed = 50;
/**
* Get the particle's position
*
* @return {Array}
*/
Object.defineProperties( this, {
"position": {
"get": function() {
_speed = undefined;
return _position;
}
}
} );
/**
* Get the particle's current speed
*
* @return {Number}
*/
Object.defineProperties( this, {
"speed": {
"get": function() {
_position = undefined;
return _speed;
}
}
} );
}
var electron = new Fermion();
electron.position; // [100, 200]
electron.speed; // Undefined
electron = new Fermion();
electron.speed; // 50
electron.position; // Undefined