Skip to content

Instantly share code, notes, and snippets.

@munro
Created November 18, 2011 05:00
Show Gist options
  • Save munro/1375645 to your computer and use it in GitHub Desktop.
Save munro/1375645 to your computer and use it in GitHub Desktop.
JavaScript Class Abstraction
// Class sugar
var Class = (function () {
'use strict';
// Base class
function Class() {
throw new Error('Cannot instantiate `Class`.');
}
// Copies another object's prototype into ours, skipping the `initialize`
// method. Giving the class control over the constructor, from there
// the parent class and mixin constructors can be called with.
function makeMixin(WrappedClass) {
return function (properties) {
var key;
for (key in properties) {
if (properties.hasOwnProperty(key) && key != 'initialize') {
WrappedClass.prototype[key] = properties[key];
}
}
};
}
// Wrap prototype method by using the self variable to bind the context
// of the method, then slicing self from the arguments.
function wrapPrototypeMethod(fn) {
return function (self) {
var args = Array.prototype.slice.call(arguments, 1);
fn.apply(self, args);
};
}
// Inheritance sugar for extending our class. It binds all of the
// prototypal methods' context to our new object.
function makeExtend(class_obj) {
return function (properties) {
var key;
function WrappedClass() {
var obj, key;
// Create new object if the `new` keyword was not used. Check
// against `global` for Node.js, and `window` for browser side
// JavaScript.
if (this === (typeof window === 'undefined' ? global : window)) {
obj = Object.create(WrappedClass.prototype);
} else {
obj = this;
}
// Set __super__ as the parent's prototype, because the user
// will want direct access to the methods for convenience.
obj.__super__ = WrappedClass.__super__.prototype;
// Copy methods & bind all methods to our new object.
for (key in WrappedClass.prototype) {
if (typeof WrappedClass.prototype[key] === 'function') {
obj[key] = Class.bind(obj, WrappedClass.prototype[key]);
}
}
// Call the constructor
if (typeof obj.initialize === 'function') {
obj.initialize();
}
// Return the constructed object if `new` keyword was not used.
return obj;
}
WrappedClass.__super__ = class_obj;
WrappedClass.extend = makeExtend(WrappedClass);
WrappedClass.mixin = makeMixin(WrappedClass);
// Mixin the passed properties. This is done because the passed
// properties aren't inheriting the parent class' prototype, but
// they need to be.
WrappedClass.prototype = Object.create(class_obj.prototype);
WrappedClass.mixin(properties);
if (properties.initialize) {
WrappedClass.prototype.initialize = properties.initialize;
}
return WrappedClass;
};
}
// Manually create the base class
Class.__super__ = Object;
Class.extend = function (arg1, arg2) {
var _extend = makeExtend(Class);
// Extend base class
if (typeof arg2 === 'undefined') {
return _extend(arg1);
// Convenience function for extending a prototype
} else {
return Class.create(arg1).extend(arg2);
}
};
Class.mixin = makeMixin(Class);
Class.bind = function (obj, fn) {
return function () {
var args = Array.prototype.slice.call(arguments, 0);
args.splice(0, 0, obj);
return fn.apply(obj, args);
};
};
// Wrap prototype to mimic a class. TODO: cache the wrapped
// prototype.
Class.create = function (prototype) {
var key;
function WrappedPrototype() {
}
WrappedPrototype.prototype = Object.create(prototype);
for(key in prototype.prototype) {
if (typeof prototype.prototype[key] === 'function') {
WrappedPrototype.prototype[key] =
wrapPrototypeMethod(prototype.prototype[key]);
}
}
return makeExtend(WrappedPrototype);
};
return Class;
}());
// Testing
(function () {
'use strict';
var Foo, Bar, Mixin, ProtoExtend, foo, bar, proto;
Foo = Class.extend({
_name: 'foo-no-name',
initialize: function (self) {
console.log('Initializing Foo:', self.name());
},
foo: function (self, a, b, c) {
console.log('Wee', a, b, c);
},
name: function (self, name) {
if (typeof name !== 'undefined') {
self._name = name;
}
return self._name;
},
check_this_out: function (self) {
console.log('Called from Foo:', self._name);
}
});
Mixin = Class.extend({
initialize: function (self) {
console.log('Initializing mixin:', self.name());
},
mixed_in_method: function (self) {
console.log('I was mixed in:', self.name());
}
});
Bar = Foo.extend({
_name: 'bar-no-name',
initialize: function (self) {
self.__super__.initialize.call(self, self);
console.log('Initializing Bar:', self.name());
Mixin.prototype.initialize.call(self, self);
},
bar: function (self, rawr) {
console.log('BAR', rawr);
},
onEvent: function (self) {
console.log('Event triggered:', self.name());
},
check_this_out: function (self) {
self.__super__.check_this_out.call(self, self);
console.log('Called from Bar:', self._name);
}
});
Bar.mixin(Mixin.prototype);
function Prototype() {
console.log('Initializing Prototype', this._name);
}
Prototype.prototype.prototypal_method = function (a, b, c) {
console.log('Prototype.prototypal_method: ', a, b, c);
};
ProtoExtend = Class.extend(Prototype, {
_name: 'proto-extend-no-name',
initialize: function (self) {
console.log('Initializing ProtoExtend');
}
});
proto = ProtoExtend();
proto.prototypal_method(1, 2, 3, 4);
foo = Foo(); // No more `new` keyword! yay!
bar = new Bar();
foo.name('foo');
bar.name('bar');
console.log(foo.name() + ', and ' + bar.name());
foo.check_this_out();
bar.check_this_out();
bar.mixed_in_method();
setTimeout(bar.onEvent, 5);
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment