JavaScript allows programmers to take properties from one object and mix them into another object. There are several ways to accomplish this mixing and a few of them are explored here.
Observable Mixin
Here is a simple example of the observer pattern that can be mixed into other objects.
var observableMethods = {
observe: function(observer) {
if (!this.hasOwnProperty('observers')) {
this.observers = [];
}
this.observers.push(observer);
},
notify: function(data) {
if (this.hasOwnProperty('observers')) {
for (var i=0, ilen=this.observers.length; i<ilen; i++) {
this.observers[i](data);
}
}
}
};
It is possible to use the
observableMethods
function as a observable itself.observableMethods.observe(function() {
alert('hi');
});
observableMethods.notify();
A little mixin function to make other things observable.
function mixinObservable(sink) {
for (var p in observableMethods) {
if (observableMethods.hasOwnProperty(p) &&
typeof observableMethods[p] === 'function') {
sink[p] = observableMethods[p];
}
}
}
We can mixin to an person created with an object literal.
var person = {
name: 'Steve',
setName: function(name) {
var oldName = this.name;
this.name = name;
this.notify({oldName: oldName, newName: this.name});
}
};
mixinObservable(person);
person.observe(function(data) {
alert(data.oldName + ' was renamed to ' + data.newName);
});
person.setName('Sarah');
Alternately we write a constructor function for observable people that we can rename.
function Person(name) {
this.setName(name);
};
mixinObservable(Person.prototype);
Person.prototype.setName = function(name) {
var oldName = this.name;
this.name = name;
this.notify({oldName:oldName, newName:this.name});
};
We can then make a person and manipulate it.
var person = new Person('Steve');
person.observe(function(data) {
alert(data.oldName + ' was renamed to ' + data.newName);
});
person.setName('Sarah');
In all of the above code, the three uses of
hasOwnProperty
are critical to understand.The first two uses of
hasOwnProperty
in observe
and notify
ensure that the object into which the methods have been mixed, will have its own set observers and not share some set of observers with any other object. The unfortunate part is that these checks run every time the observe
and notify
methods are called. This is inefficient and something we want to fix.The third use of
hasOwnProperty
in mixinObservable
means that only properties directly on observableMethods
will be mixed into other objects. This is important because if we do not use hasOwnProperty
then we copy all of the enumerable Object.prototype
properties which is either wasteful or will overwrite the custom methods with the same names on other objects.
Fixing the Inefficiency
The inefficiency in
observe
and notify
can be fixed by making a constructor function for Observable
objects.function Observable() {
this.observers = [];
}
Observable.prototype.observe = function(observer) {
this.observers.push(observer);
};
Observable.prototype.notify = function(data) {
for (var i=0, ilen=this.observers.length; i<ilen; i++) {
this.observers[i](data);
}
};
Observable.call(Observable.prototype);
Now the
observe
and notify
methods of an observable function are more efficient as we know the observers
property was created when the Observable
constructor function ran.The last line,
Observable.call(Observable.prototype);
, is a bit of an unusual one but it makes it possible to observe Observable.prototype
just like observableMethods
.A new mixin function.
function mixinObservable(sink) {
for (var p in Observable.prototype) {
if (Observable.prototype.hasOwnProperty(p) &&
typeof Observable.prototype[p] === 'function') {
sink[p] = Observable.prototype[p];
}
}
Observable.call(sink); // optional depending what you want
}
Mixing into a person created with an object literal just like it was done above. The last line of this new
mixinObservable
insures the observers
property is created.var person = {
name: 'Steve',
setName: function(name) {
var oldName = this.name;
this.name = name;
this.notify({oldName: oldName, newName: this.name});
}
};
mixinObservable(person);
person.observe(function(data) {
alert(data.oldName + ' was renamed to ' + data.newName);
});
person.setName('Sarah');
The trick comes when we create a
Person
constructor function.function Person(name) {
Observable.call(this);
this.setName(name);
};
mixinObservable(Person.prototype);
Person.prototype.setName = function(name) {
var oldName = this.name;
this.name = name;
this.notify({oldName:oldName, newName:this.name});
};
The first line of the constructor function,
Observable.call(this);
, ensures that each Person
object has its own set of observers. Without this call, all the people will share the same list of observers which is the set of observers on the Person.prototype
object. If this makes you squint then it is well worth the effort to think about it until it is clear why.To use some class vocabulary, the first line of the constructor function can be thought of as a
super
call and that the Person class inherits from the Observable class. Some JavaScript diehards cringe at the mention of this vocabulary but I think the comparison is worth consideration.
Multiple Mixins
Multiple mixins follow the same pattern.
function Person(name) {
Observable.call(this);
Common.call(this);
this.setName(name);
}
mixinObservable(Person.prototype);
mixinCommon(Person.prototype);
// By coincidence mixinCommon also added a notify method which
// clobbered the method of the same name added by mixinObservable.
// Fix this problem making appropriate decisions about how
// to call both.
Person.prototype.notify = function() {
Common.prototype.notify.call(this);
Observable.prototype.notify.apply(this, arguments);
};
// ...
Prototype Chaining vs. Mixins
There is one primary difference between prototype chaining and mixins. For example,
function Person(name) {
Observable.call(this);
Base.call(this);
this.setName(name);
}
// chain prototypes so that all Person objects
// inherit from Base.prototype.
Person.prototype = Object.create(Base.prototype);
Person.prototype.constructor = Person;
Base.call(Person.prototype); // optional depending what you want
mixinObservable(Person.prototype);
Now if we add methods to both
Base.prototype
and Observable.prototype
after the above code has executed. Only the method added to Base.prototype
will be added to Person
objects.Base.prototype.getId = function() {/*...*/};
Observable.prototype.getObservers = function() {/*...*/};
var person = new Person('Steve');
person.getId(); // ok
person.getObservers(); // error: getObservers not defined
Enjoy your mixins.
No comments:
Post a Comment
Thank's!