A little over a year ago, I wrote an article about how to do data binding in pure JavaScript. Anyone that read my ramblings about data binding in pure JavaScript probably would know that I’m not a huge fan of frameworks, but to say I hate them would be an overstatement. But because I like minimalism in JavaScript, knowing how to implement some basic patterns can enable to use JavaScript without having to import a framework with all its baggage, and one of those patterns is the Observer pattern.
The Observer pattern and its similar cousin the Publisher/Subscriber (a.k.a. Pub/Sub) pattern are elegantly simple and profoundly useful in all kinds of applications. The Observer pattern works by one object — the observer — subscribing to another object — the subject — by way of a callback method. Whenever an event occurs on the subject, it notifies the observer that something happened.
Observers makeup the backbone of data-binding, which underscores major application level architectures in the Model-View-Whatever (MV) family such as MVC, MVVM, and MVP. These have been used in most every modern application for the last two decades. JavaScript has seen its fair share of MV frameworks over the years such as Knockout, Ember, Backbone, and Angular. These frameworks fundamentally use some kind of observer pattern that makes the data binding work.
Observing Objects
In other programming languages, the observer pattern is typically more done by way of an interface wherein the subject will implement will implement that interface. JavaScript is prototypal, meaning it uses prototypes instead of classes to define objects, so interfaces aren’t really an option. However, it is possible to use some features of JavaScript to make it possible, and perhaps even more dynamic, in that literally any object can be observed, even if it isn’t explicitly defined as such. In the article on data binding, the pattern was present, it just wasn’t explicitly implemented. Using the same basic structure with Object.defineProperty method, one can hook into a properties setter and call a subscriber method that gets “notified” when the property changes.
function Observer(o, property){ var _this = this; var value = o[property]; this.observers = []; this.Observe = function (notifyCallback){ _this.observers.push(notifyCallback); } Object.defineProperty(o, property, { set: function(val){ _this.value = val; for(var i = 0; i < _this.observers.length; i++) _this.observers[i](val); }, get: function(){ return _this.value; } }); }
Now with this simple constructor, you can pass in any object and the property you want to observe. After the Observer is created, anything can hook into the observer with a notify function, which will be called whenever the property is set.
You can play with this here on JSFiddle.
Observing Arrays
Arrays in JavaScript a bit more of a challenge, but because they themselves are a type of object it possible to make them observable too. Like other methods in objects, the native methods in JavaScript arrays can be overridden. But to keep it simple and not have to reimplement the code in the methods, JavaScript prototyping comes to the rescue. A JavaScript has a special method called “apply” which essentially lets another object “borrow” the method. To keep from having to re-implement the code for every method in the array, the overridden method can “borrow” from the prototype object with and apply the methods to the array, such as push, pop, sort, slice, and shift. Once the prototypes methods are borrowed, the code to call the notify methods can be added. The ArrayObserver constructor will look something like this.
function ArrayObserver(a){ var _this = this; this.array = a; this.observers = []; this.Observe = function (notifyCallback){ _this.observers.push(notifyCallback); } a.push = function(obj){ var push = Array.prototype.push.apply(a, arguments); for(var i = 0; i < _this.observers.length; i++) _this.observers[i](obj, "push"); return push; } a.pop = function(){ var popped = Array.prototype.pop.apply(a, arguments); for(var i = 0; i < _this.observers.length; i++) _this.observers[i](popped, "pop"); return popped; } a.reverse = function() { var result = Array.prototype.reverse.apply(a, arguments); for(var i = 0; i < _this.observers.length; i++) _this.observers[i](result, "reverse"); return result; }; a.shift = function() { var deleted_item = Array.prototype.shift.apply(a, arguments); for(var i = 0; i < _this.observers.length; i++) _this.observers[i](deleted_item, "shift"); return deleted_item; }; a.sort = function() { var result = Array.prototype.sort.apply(a, arguments); for(var i = 0; i < _this.observers.length; i++) _this.observers[i](result, "sort"); return result; }; a.splice = function(i, length, itemsToInsert) { var returnObj if(itemsToInsert){ Array.prototype.slice.call(arguments, 2); returnObj = itemsToInsert; } else{ returnObj = Array.prototype.splice.apply(a, arguments); } for(var i = 0; i < _this.observers.length; i++) _this.observers[i](returnObj, "splice"); return returnObj; }; a.unshift = function() { var new_length = Array.prototype.unshift.apply(a, arguments); for(var i = 0; i < _this.observers.length; i++) _this.observers[i](new_length, "unshift"); return arguments; }; }
This implementation is using one observer method for all of the functions, and that method is being called with the function that was performed and the results of that function.
You can play with this one too over on JSFiddle.
With a property Observer and an ArrayObserver in JavaScript, adding on middleware for GUI stuff to manipulate the DOM is pretty straightforward.
So there you have it: an Observer and ArrayObserver in Pure JavaScript.
Atmosera: Azure Cloud Solutions Provider