JavaScript: Define Object Properties
I recently learned about a different way of defining object properties in JavaScript that has helped me write a bit cleaner code: Object.defineProperty
. Let's take a look at some basic uses!
In this example we have an application that has a service that manages our current user. However, this application needs to be able to send notifications to the rest of the app when the user has been "set" and when it has been "authenticated". In an offline context, for instance, there may not be a way to authenticate a user retrieved from a cookie, etc..., so these need to be separate properties and events. One way to do this is to use the concept of getter/setter functions:
(function(app, evt){
var _user, _isAuthenticated;
var authenticatedUserSvc = {};
authenticatedUserSvc.user = function(user) {
if(user) {
_user = user;
evt.triggerEvent("user:set", _user);
}
return _user;
};
authenticatedUserSvc.isAuthenticated = function(isAuth) {
if(!!isAuth && !_user) {
//Prevent the service from being told the user is authenticated if there is no user...
throw 'Cannot set isAuthenticated to true if there is no authenticated user defined';
}
if(isAuth.toString().length > 0) {
_isAuthenticated = isAuth;
if(_isAuthenticated) {
evt.triggerEvent('user:authenticated', _user);
}
}
return _isAuthenticated;
};
myApp.authenticatedUserSvc = authenticatedUserSvc;
}(myApp, myEvtSvc));
So, what's wrong with that?
While this contains the appropriate validation logic, triggers our events, etc..., it causes its interface to be such that consumers of the service must know to invoke these properties as functions:
//GET
if( myApp.authenticatedUserSvc.isAuthenticated() ) {
/*do stuff...*/
}
//SET
myApp.authenticatedUserSvc.user({name: 'bradley'});
While there's nothing inherintly wrong with doing it this way, it does cause consumers a couple more keystrokes and, to me, it lessens the readability of the code. I prefer to read something like user.name = 'Joe'
over user.name('Joe')
whenever possible.
If we were to refactor this service to use Object.defineProperty
, then we could have the best of both worlds! We can keep our custom logic, and anything interacting with this interface gets to treat it just like any other object!
How it works:
Object.defineProperty
is a function that takes 3 arguments:
- object reference - reference to the object you want to add a property to
- property name - what you want the property to be called: i.e 'user'
- property definition object - the definition of the property. This definition can contain multiple properties defining how this new property can be interacted with.
Here's an example of applying custom get/set functions to properties:
(function (app,evt) {
var _user, _isAuthenticated, userSvc = {};
Object.defineProperty(userSvc, 'authenticatedUser', {
get: function() {
return _user;
},
set: function(user) {
_user = user;
evt.triggerEvent('user:set', _user);
}
});
Object.defineProperty(userSvc, 'isAuthenticated', {
get: function() {
return _isAuthenticated;
},
set: function(isAuthenticated) {
if(!!isAuthenticated && !_user) {
throw 'Cannot set isAuthenticated to true if there is no authenticated user defined';
}
_isAuthenticated = isAuthenticated;
if(_isAuthenticated) {
evt.triggerEvent('user:authenticated', _user);
}
}
});
}(myApp, myEvtSvc));
That's nice. Now, anything that interacts with this service can use it like this:
//GET the user:
var theUser = myApp.authenticatedUserService.user;
//SET the user:
myApp.authenticatedUserService.user = someUserObject;
//GET the isAuthenticated value
if( myApp.authenticatedUserService.isAuthenticated ) {
/*do stuff...*/
}
//SET the isAuthenticated value
myApp.authenticatedUserService.isAuthenticated = true;
This example is just scratching the surface of the capabilities of using Object.defineProperty
. You can use this approach to build read-only properties, properties that are not enumerable when enumerating the properties of an object - for (var prop in someObject) {/*do stuff...*/}
, and much more! For a deeper dive, MDN has excellent documentation on Object.defineProperty
.