Last active
December 15, 2015 12:48
-
-
Save lsm/5262363 to your computer and use it in GitHub Desktop.
Feature rich Javascript OO implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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