Skip to content

Instantly share code, notes, and snippets.

@lsm
Last active December 15, 2015 12:48
Show Gist options
  • Save lsm/5262363 to your computer and use it in GitHub Desktop.
Save lsm/5262363 to your computer and use it in GitHub Desktop.
Feature rich Javascript OO implementation
/**
* Javascript OO helpers
*/
/**
* Module exports.
*/
exports.Base = Base;
/**
* Feature rich javascript OO implementation
* @param superCtor {Function|Object} Optional parent class's constructor or module object
* @param subModule {Object} Instance properties of subclass
* @return {Function}
* inspired by:
* http://rightjs.org/docs/class
* http://ejohn.org/blog/simple-javascript-inheritance/
*/
function Base(superCtor, subModule) {
var superClass;
if (typeof superCtor !== 'function') {
subModule = superCtor;
superCtor = function () {
};
}
if (typeof subModule === "function") {
superClass = subModule;
subModule = superClass.prototype;
}
var global = this;
// constructor
var ctor = function (module) {
// determine if current function was called directly or with a new operator
if (this !== global) {
// function is called in order to initialize a instance
if (superClass) {
superClass.call(this);
}
this.init.apply(this, arguments);
} else {
// ctor is called in order to define a subclass
return Base(ctor, module);
}
};
ctor.include = function () {
extend.call(ctor, ctor.prototype, arguments);
return ctor;
};
ctor.extend = function () {
return extend.call(ctor, ctor, arguments);
};
ctor = inherits(ctor, superCtor);
if (!ctor.prototype.init) {
ctor.prototype.init = superCtor;
}
// extending the prototype
if (subModule) {
extend.call(ctor, ctor.prototype, [subModule]);
}
ctor.prototype.extend = function () {
return extend.call(ctor, this, arguments);
};
ctor.constructor = ctor; // Class.constrcutor == Class
ctor.prototype.constructor = ctor; // instance.constructor == Class
return ctor;
}
/**
* Helper functions for Base
*/
function makeSuper(name, fn, oldSuper) {
return function () {
var tmp = this._super;
this._super = oldSuper;
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
}
function extend(target, modules) {
for (var i = 0, len = modules.length; i < len; i++) {
var subModule = modules[i];
switch (typeof subModule) {
case 'function':
// a constructor function
subModule = subModule.prototype;
break;
case 'object':
if (subModule === null) {
continue;
}
// a module like object
if (subModule.hasOwnProperty("include")) {
if (Array.isArray(subModule.include)) {
this.include.apply(null, subModule.include);
} else {
this.include(subModule.include);
}
delete subModule.include;
}
if (subModule.hasOwnProperty("extend")) {
if (Array.isArray(subModule.extend)) {
this.extend.apply(null, subModule.extend);
} else {
this.extend(subModule.extend);
}
delete subModule.extend;
}
break;
default:
throw new Error("Type not accepted");
}
for (var name in subModule) {
if (typeof target[name] === "function" && typeof subModule[name] === "function") {
var _super = target[name];
target[name] = makeSuper(name, subModule[name], _super);
continue;
}
target[name] = subModule[name];
}
}
return target;
}
// copied from sys.inherits
function inherits(target, parent) {
var tempCtor = function () {
};
tempCtor.prototype = parent.prototype;
target.prototype = new tempCtor();
return target;
}
function mocha() {
function setup() {
Class = Base(function (name) {
this.name = name;
});
ClassPlus = Class({
init: function (name, age) {
this._super(name + 1);
this.age = age;
}
});
ClassPlusPlus = ClassPlus({
getName: function () {
return this.name;
},
getAge: function () {
return this.age;
},
include: [
{getSchool: function () {
return 'MIT';
}}
],
extend: [
{getSchool: function () {
return 'NYU';
}}
]
});
Class4 = ClassPlusPlus({
init: function (name, age) {
this._super(name, age);
},
// instance method
include: {
setAge: function (age) {
this.age = age;
}
},
// static method
extend: {
getClassName: function () {
return 'Class4';
}
}
});
klass = new Class('class');
klassPlus = new ClassPlus('classPlus', 18);
klassPlusPlus = new ClassPlusPlus('classPlusPlus', 26);
klass4 = new Class4('class4', 14);
}
describe('Base', function () {
it('should inherits', function () {
setup();
assert.equal(klass.constructor, Class);
assert.equal(klass instanceof Class, true);
assert.equal(klassPlus.constructor, ClassPlus);
assert.equal(klassPlus instanceof Class, true);
assert.equal(klassPlus instanceof ClassPlus, true);
assert.equal(klassPlusPlus.constructor, ClassPlusPlus);
assert.equal(klassPlusPlus instanceof Class, true);
assert.equal(klassPlusPlus instanceof ClassPlus, true);
assert.equal(klassPlusPlus instanceof ClassPlusPlus, true);
assert.equal(undefined, ClassPlus.prototype.getAge);
assert.equal(undefined, klassPlus.getName);
});
it('should call constructor on initialization', function () {
assert.equal(klass.name, 'class');
assert.equal(klassPlus.age, 18);
assert.equal(klassPlus.name, 'classPlus1');
assert.equal(klass4.getAge(), 14);
klass4.setAge(15);
assert.equal(klass4.getAge(), 15);
assert.equal(klass4.getName(), 'class41');
assert.equal(klassPlusPlus.getName(), 'classPlusPlus1');
assert.equal(klassPlusPlus.getAge(), 26);
});
it('should extend constructor and instance', function () {
ClassPlus.extend({
hello: function () {
return 'hello';
}
}, {
world: function () {
return 'world';
}
});
// static functions
assert.equal(ClassPlus.hello(), 'hello');
assert.equal(ClassPlus.world(), 'world');
assert.equal(ClassPlusPlus.hello, undefined);
assert.equal(ClassPlusPlus.getSchool(), 'NYU');
assert.equal(Class.hello, undefined);
assert.equal(Class4.getClassName(), 'Class4');
assert.equal(klassPlus.hello, undefined);
setup();
klassPlusPlus.extend({
getEmail: function () {
return this.email;
},
setEmail: function (email) {
this.email = email;
}
});
klassPlusPlus.setEmail('123@123.com');
assert.equal(klassPlusPlus.getEmail(), '123@123.com');
assert.equal(klassPlusPlus.getSchool(), 'MIT');
assert.equal(klassPlus.setEmail, undefined);
assert.equal(klass4.setEmail, undefined);
assert.equal(Class4.setEmail, undefined);
assert.equal(ClassPlus.setEmail, undefined);
});
it('should convert module to class', function () {
var Engine = Base({
init: function (name) {
this.name = name;
},
start: function () {
return 'engine ' + this.name + ' started';
}
});
var engine = new Engine('e90');
assert.equal(engine instanceof Engine, true);
assert.equal(engine.start(), 'engine e90 started');
});
it('should extend class like a module', function () {
var Person = function (name) {
this.name = name;
};
var Worker = function () {
this.role = 'worker';
};
Worker.prototype.work = function () {
return this.role + ' can work';
};
var WorkerClass = Base(Person, Worker);
var worker = new WorkerClass('john');
assert.equal(worker.name, 'john');
assert.equal(worker.role, 'worker');
assert.equal(worker.work(), 'worker can work');
var Leader = function () {
this.role = 'leader';
};
Leader.prototype.getRole = function () {
return this.role;
};
WorkerClass.include(Leader); // `Leader` will be treated as a module
var leadWorker = new WorkerClass('tom');
assert.equal(leadWorker.getRole(), 'worker');
});
it('should throw exception when extending wrong object', function () {
try {
Class.include('');
assert.equal(1, 2); // should not be called
} catch (e) {
assert.equal(e.message, 'Type not accepted');
}
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment