The Class Class
At Digg we use jQuery extensively, but it doesn't offer a much in the way of Object-Oriented JavaScript. Existing OOJS libraries weren't a good fit -- often trying to shoehorn traditional OOP patterns in where they don't quite fit -- so we rolled our own.
Enjoy!
Part 1: Creating and Manipulating Classes and Namespaces
Hello World
Simplest usage, creates a dynamic class in the current scope ``` var MyClass = Class.create();
var mc = new MyClass(); ```
Static / Singleton
And now, a static class. Anytime the last argument passed to Class.create is boolean, it'll serve as the static switch (defaults to false). ``` var MyClass = Class.create(true);
MyClass.namespace('foo'); ```
Adding the Constructor and Methods
Ok, so we have classes now, but their constructors and prototypes are empty. We could use $.extend, but why not make it a bit tidier? You can pass any number of objects to Class.create and they'll be tacked onto your class' prototype (dynamic) or the class itself (static). The 'init' method serves as each Class' constructor. ``` var method = { someFunc: function(){} }
var MyClass = Class.create({ init: function(){ console.log('You instantiated a Class!'); }, myFunc: function(){}, myProp: 'foo' }, method);
var foo = new MyClass();
foo.someFunc(); ```
Namespaces
Alright, so we have classes now, and they have methods. What's next? Namespaces. Class.namespace is a badass object manipulation method. If you pass it a string, it'll turn that string into a static class inside the current one. ``` var MyClass = Class.create(true);
//this... MyClass.namespace('bar');
//...is the same as this $.extend(MyClass, { bar: Class.create(true); }); ```
Deep Namespaces
But wait, there's more. You can pass in a dot-notated String for more fun. Note: existing properties will not be overwritten. If MyClass.foo exists, MyClass.namespace('foo.bar') will keep foo as is and copy bar into it, though you should really use MyClass.foo.namespace('bar') at that point. ``` var MyClass = Class.create(true);
//this... MyClass.namespace('bar.baz.quux');
//...is the same as this $.extend(MyClass, { bar: Class.create(true); });
$.extend(MyClass.bar, { baz: Class.create(true); });
$.extend(MyClass.bar.baz, { quux: Class.create(true); }); ```
Existing Global Namespaces
Annnd more: If a namespace already exists as an object in the global scope, it'll be copied into your class. This is handy if you have an existing object or class sitting around and you want to integrate it into your obsessive/compulsive organization scheme. ``` var MyClass = Class.create(true);
var stuff = Class.create({ things: function(){} }, true);
MyClass.namespace('bar.stuff');
//MyClass.bar.stuff.things.constructor == Function ```
Namespace / Method Combo Object
But wait, ''there's more'': Pass in an object and it'll run through and copy its properties that are Objects or Functions into your Class. ``` var MyClass = Class.create(true);
var stuff = Class.create({ things: function(){} }, true);
MyClass.namespace({ stuff: { things: function(){} } });
//MyClass.stuff.things.constructor == Function ```
Multiple Namespaces With an Array
Want to add multiple namespaces at the same level? Pass in an array of strings or objects: ``` var MyClass = Class.create(true);
MyClass.namespace([ 'foo.bar',
{
baz: {
doStuff: function(){}
}
},
'quux'
]); ```
Dynamic Classes in Namespaces
Phew. Ok then. So we have Classes, they have methods and namespaces (which are, by default, static classes). Next up: Dynamic classes inside namespaces. For this we'll use the version of Class.create that gets prototyped onto all classes (so MyClass.create, not Class.create). It differs from the main one in that you pass it a name for the inner Class before all of the regular arguments, like so: ``` var MyClass = Class.create(true);
MyClass.create('MyOtherClass', { myOtherFunc: function(){} });
var foo = new MyClass.MyOtherClass();
foo.myOtherFunc(); ```
The Terrible Secret of Space
Let's put it all together for one last demo: ``` var D = Class.create(true);
D.namespace('estroy.humans');
D.estroy.humans.create('Now', { rawr: function() { console.log('BOOM! SMASH! POW!'); } });
var foo = new D.estroy.humans.Now();
foo.rawr(); ```
Part 2: Extending Classes
First, let's talk about what happens when you extend a class.
``` var Foo = Class.create({ fooz: function() { alert("Foo's fooz was called"); } });
var Bar = Class.create(Foo.prototype, { fooz: function() { alert("Bar's fooz was called"); } }); ```
You could say that we just extended Foo with Bar, and for convenience's sake we'll refer to this process as "extending" a class, but that's not quite what's really going on. What we've done here is we've created the class Bar with a copy of Foo's prototype, and overwritten Foo.prototype.fooz.
Remember, JavaScript doesn't use classical inheritance. We have no formal classes, nor subs, nor supers. What we have is Objects, their prototypes and a perverse need to wrangle them into a pattern that resembles normal OOP. For that reason, when Class sees a duplicate property whose constructor is Function, it will copy that method into an object called 'supers' before overwriting it.
The effect of that copy is this:
``` var Foo = Class.create({ fooz: function() { alert("Foo's fooz was called"); } });
var Bar = Class.create(Foo.prototype, { fooz: function() { this.sup(); alert("Bar's fooz was called"); } }); ```
The call above to "this.sup()" (The word 'super' wasn't used because it's reserved in JS 2.0) invokes the copy of Foo.prototype.fooz that was stored in Bar.prototype.supers. Calling Bar.prototype.fooz() will now trigger both Foo and Bar's alerts. What we have here, in essence, is a very simple connection between a sub-class and its super.
It comes with a significant caveat though: It only works one level above the current class, each time you extend a class and overwrite one of its methods, only the last duplicate method is stored in the "supers" object.
Consider the following:
``` var Foo = Class.create({ fooz: function() { console.log('fooz of Foo'); } });
var Bar = Class.create(Foo.prototype, { fooz: function() { this.sup(); console.log('fooz of Bar'); } });
var Baz = Class.create(Bar.prototype, { fooz: function() { this.sup(); console.log('fooz of Baz'); } }); ```
Baz.prototype.supers.fooz now contains code that invokes "this.sup", which is a reference to Baz.prototype.supers.fooz. Enjoy your beachball!
In the future we may add support for inheriting as far down as you'd care to go, but for now this single level adds as much classical inheritance as we'd care to see in JavaScript.