src/clazz.js
// EnoFJS
// Version: 4.0.0
//
// Copyright (c) 2014.
//
// Author Andy Tang
// Fork me on Github: https://github.com/EnoF/EnoFJS
(function ClassScope(undefined) {
'use strict';
// A map containing all classes registered to the ClassFactory.
// These classes are the original classes.
var registeredClasses = {};
// A map containing all generated classes.
// We will use this to look up and manage the dependency of the sup scope.
var registeredEnoFJSClasses = {};
// Wrap up a given class into an EnoFJS class.
// clazz(function Animal(){
// ...
// });
function clazz(className, NewClass) {
if (typeof className === 'function') {
NewClass = className;
className = NewClass.extractFunctionName();
}
return parseToPrototypedClass(className, NewClass);
}
// Parse a normal class into a Prototyped Class.
function parseToPrototypedClass(className, NewClass) {
var instance = normalizeInstance(new NewClass());
var parent;
var parentProto;
// Generate Getters and Setters into the prototype.
generateAutoIsGetSet('private', instance.private, instance.public);
generateAutoIsGetSet('protected', instance.protected, instance.public);
if (instance.extend !== undefined) {
// Merge the parent properties into this instance.
parent = registeredClasses[instance.extend];
parentProto = extendParent(instance, parent);
}
// Create a prototyped class based on the instance.
var PrototypedClass = createProtoClass(instance, parent);
if (instance.extend !== undefined) {
// Create cross references for the extended members.
crossReferenceExtendedMembers(PrototypedClass, parentProto);
}
// Register the new PrototypedClass for future extension.
registeredClasses[className] = PrototypedClass;
// Create a new EnoFJSClass out of the PrototypedClass.
return createNewEnoFJSClass(className, PrototypedClass);
}
function extendParent(instance, parent) {
// Create a new scope of the Private, Protected and Public,
// so that the instances won't clash.
var parentProto = {
private: new parent.Private(),
protected: new parent.Protected(),
public: new parent.Public(),
constructor: parent.constructor
};
// Merge the missing members of the parent into this class.
mergeParentIntoChild(parentProto.private, instance.private);
mergeParentIntoChild(parentProto.protected, instance.protected);
mergeParentIntoChild(parentProto.public, instance.public);
return parentProto;
}
function createProtoClass(instance, parent) {
// For each scope, we need to create a new instance. Otherwise it will clash when the
// class is instantiated multiple times!
function Private() {}
Private.prototype = instance.private;
function Protected() {}
Protected.prototype = instance.protected;
function Public() {}
Public.prototype = instance.public;
// The Prototyped Class Object.
var PrototypedClass = {
extend: instance.extend,
constructor: instance.constructor,
sup: parent !== undefined ? parent.constructor :
/* istanbul ignore next: only for safety reasons */
function noopConstructor() {},
Private: Private,
Protected: Protected,
Public: Public
};
return PrototypedClass;
}
function crossReferenceExtendedMembers(PrototypedClass, parentProto) {
// Each scope can only access the parent's same scope.
PrototypedClass.Private.prototype.sup = parentProto.private;
PrototypedClass.Protected.prototype.sup = parentProto.protected;
PrototypedClass.Public.prototype.sup = parentProto.public;
// The references should be referenced to this instance, rather than the parent instance.
createCrossReference(PrototypedClass, PrototypedClass.Private.prototype.sup);
createCrossReference(PrototypedClass, PrototypedClass.Private.prototype.sup);
createCrossReference(PrototypedClass, PrototypedClass.Public.prototype.sup);
}
// Generate is, get and setters. You can combine the `get` or `is` with the `set`.
// i.e. `getSet` or `isSet`.
// this.private = {
// foo: {
// get: 'value'
// },
// bar: {
// is: true
// },
// baz: {
// getSet: 'value'
// }
// }
function generateAutoIsGetSet(scopeName, members, publicScope) {
for (var member in members) {
var autoProperty = members[member];
var getter = false;
var isser = false;
var capitalizedMemberName = member.capitaliseFirstLetter();
if (!(autoProperty instanceof Object)) {
continue;
}
if (hasGet(autoProperty)) {
// Set the value on the member, when it was a getSet, it will be set later on.
members[member] = autoProperty.get;
// Generate a getter.
publicScope['get' + capitalizedMemberName] = generateAutoGet(scopeName, member);
getter = true;
} else if (hasIs(autoProperty)) {
// Set the value on the member, when it was a isSet, it will be set later on.
members[member] = autoProperty.is;
// Generate an isser.
publicScope['is' + capitalizedMemberName] = generateAutoIs(scopeName, member);
isser = true;
}
if (hasSet(autoProperty)) {
// Set the value on the member, depending if it is a set, getSet or isSet.
if (getter) {
members[member] = autoProperty.getSet;
} else if (isser) {
members[member] = autoProperty.isSet;
} else {
members[member] = autoProperty.set;
}
// Generate a setter.
publicScope['set' + capitalizedMemberName] = generateAutoSet(scopeName, member);
}
}
}
// Generate a getter.
function generateAutoGet(scopeName, member) {
return function autoGet() {
return this[scopeName][member];
};
}
// Generate a isser.
function generateAutoIs(scopeName, member) {
return function autoIs() {
return this[scopeName][member];
};
}
// Generate a setter.
function generateAutoSet(scopeName, member) {
return function autoSet(value) {
this[scopeName][member] = value;
};
}
// Merge parent functions into child scope.
function mergeParentIntoChild(parent, child) {
for (var member in parent) {
if (!child.hasOwnProperty(member)) {
child[member] = parent[member];
}
}
}
// Make public functions available in the `this` scope.
function publish(scope, publicMembers) {
for (var member in publicMembers) {
scope[member] = publicMembers[member];
}
}
// Create a new EnoFJS class.
function createNewEnoFJSClass(className, PrototypedClass) {
function EnoFJSClass() {
// Instantiate the Prototyped Class.
var instance = {
private: new PrototypedClass.Private(),
protected: new PrototypedClass.Protected(),
public: new PrototypedClass.Public(),
constructor: PrototypedClass.constructor,
sup: PrototypedClass.sup
};
// Create references from scope to scope.
createCrossReference(instance, instance.private);
createCrossReference(instance, instance.protected);
createCrossReference(instance, instance.public);
// Merge the public object into the this of the EnoFJS class.
publish(this, instance.public);
// Trigger the constructor.
instance.constructor.apply(instance, arguments);
}
// Make the class related to the parent.
// Dog instanceof Animal === true;
if (PrototypedClass.extend !== undefined) {
EnoFJSClass.prototype = new registeredEnoFJSClasses[PrototypedClass.extend]();
}
// Register the EnoFJS class so we can extend from it.
registeredEnoFJSClasses[className] = EnoFJSClass;
return EnoFJSClass;
}
function createCrossReference(referenceObject, instanceScope) {
// The reference object can be an instance or the PrototypeClass.
instanceScope.private = referenceObject.private || referenceObject.Private.prototype;
instanceScope.protected = referenceObject.protected || referenceObject.Protected.prototype;
instanceScope.public = referenceObject.public || referenceObject.Public.prototype;
}
// Normalizing the instance in order to skip checks in the future.
function normalizeInstance(instance) {
instance.private = instance.private || {};
instance.protected = instance.protected || {};
instance.public = instance.public || {};
instance.sup = instance.sup || {};
instance.constructor = instance.constructor ||
/* istanbul ignore next: this is only for safety reasons */
function constructor() {};
return instance;
}
// Extract the function name so we can use this to determine the name of the class
// we want to register. By extracting the name of the function, the registration of the class
// doesn't require a string to be passed on as a parameter.
Function.prototype.extractFunctionName = function extractFunctionName() {
var functionName = this.toString();
functionName = functionName.substr('function '.length);
functionName = functionName.substr(0, functionName.indexOf('('));
return functionName;
};
// When an innerfunction will be passed down to an other function, it could be that the scope
// will be modified. Think of event handlers.
// Now you can pass the function with `this.private.getFoo.bind(this)`. This will preserve the scope.
Function.prototype.bindScope = function bindScope(scope) {
var self = this;
return function boundScope() {
return self.apply(scope, arguments);
};
};
// **Getters, Setters and Issers**
// `get || getSet`
function hasGet(value) {
return value.hasOwnProperty('get') || value.hasOwnProperty('getSet');
}
// `set || getSet || isSet`
function hasSet(value) {
return value.hasOwnProperty('set') || value.hasOwnProperty('getSet') ||
value.hasOwnProperty('isSet');
}
// `is || isSet`
function hasIs(value) {
return value.hasOwnProperty('is') || value.hasOwnProperty('isSet');
}
// To easily camel case our generated functions :)
String.prototype.capitaliseFirstLetter = function capitaliseFirstLetter() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
// Publish the module to the available source.
window.enofjs = {
clazz: clazz
};
}());