declare.js
Version:
OO system from node and browser
926 lines (879 loc) • 31.3 kB
JavaScript
(function () {
/**
* @projectName declare
* @github http://github.com/doug-martin/declare.js
* @header
*
* Declare is a library designed to allow writing object oriented code the same way in both the browser and node.js.
*
* ##Installation
*
* `npm install declare.js`
*
* Or [download the source](https://raw.github.com/doug-martin/declare.js/master/declare.js) ([minified](https://raw.github.com/doug-martin/declare.js/master/declare-min.js))
*
* ###Requirejs
*
* To use with requirejs place the `declare` source in the root scripts directory
*
* ```
*
* define(["declare"], function(declare){
* return declare({
* instance : {
* hello : function(){
* return "world";
* }
* }
* });
* });
*
* ```
*
*
* ##Usage
*
* declare.js provides
*
* Class methods
*
* * `as(module | object, name)` : exports the object to module or the object with the name
* * `mixin(mixin)` : mixes in an object but does not inherit directly from the object. **Note** this does not return a new class but changes the original class.
* * `extend(proto)` : extend a class with the given properties. A shortcut to `declare(Super, {})`;
*
* Instance methods
*
* * `_super(arguments)`: calls the super of the current method, you can pass in either the argments object or an array with arguments you want passed to super
* * `_getSuper()`: returns a this methods direct super.
* * `_static` : use to reference class properties and methods.
* * `get(prop)` : gets a property invoking the getter if it exists otherwise it just returns the named property on the object.
* * `set(prop, val)` : sets a property invoking the setter if it exists otherwise it just sets the named property on the object.
*
*
* ###Declaring a new Class
*
* Creating a new class with declare is easy!
*
* ```
*
* var Mammal = declare({
* //define your instance methods and properties
* instance : {
*
* //will be called whenever a new instance is created
* constructor: function(options) {
* options = options || {};
* this._super(arguments);
* this._type = options.type || "mammal";
* },
*
* speak : function() {
* return "A mammal of type " + this._type + " sounds like";
* },
*
* //Define your getters
* getters : {
*
* //can be accessed by using the get method. (mammal.get("type"))
* type : function() {
* return this._type;
* }
* },
*
* //Define your setters
* setters : {
*
* //can be accessed by using the set method. (mammal.set("type", "mammalType"))
* type : function(t) {
* this._type = t;
* }
* }
* },
*
* //Define your static methods
* static : {
*
* //Mammal.soundOff(); //"Im a mammal!!"
* soundOff : function() {
* return "Im a mammal!!";
* }
* }
* });
*
*
* ```
*
* You can use Mammal just like you would any other class.
*
* ```
* Mammal.soundOff("Im a mammal!!");
*
* var myMammal = new Mammal({type : "mymammal"});
* myMammal.speak(); // "A mammal of type mymammal sounds like"
* myMammal.get("type"); //"mymammal"
* myMammal.set("type", "mammal");
* myMammal.get("type"); //"mammal"
*
*
* ```
*
* ###Extending a class
*
* If you want to just extend a single class use the .extend method.
*
* ```
*
* var Wolf = Mammal.extend({
*
* //define your instance method
* instance: {
*
* //You can override super constructors just be sure to call `_super`
* constructor: function(options) {
* options = options || {};
* this._super(arguments); //call our super constructor.
* this._sound = "growl";
* this._color = options.color || "grey";
* },
*
* //override Mammals `speak` method by appending our own data to it.
* speak : function() {
* return this._super(arguments) + " a " + this._sound;
* },
*
* //add new getters for sound and color
* getters : {
*
* //new Wolf().get("type")
* //notice color is read only as we did not define a setter
* color : function() {
* return this._color;
* },
*
* //new Wolf().get("sound")
* sound : function() {
* return this._sound;
* }
* },
*
* setters : {
*
* //new Wolf().set("sound", "howl")
* sound : function(s) {
* this._sound = s;
* }
* }
*
* },
*
* static : {
*
* //You can override super static methods also! And you can still use _super
* soundOff : function() {
* //You can even call super in your statics!!!
* //should return "I'm a mammal!! that growls"
* return this._super(arguments) + " that growls";
* }
* }
* });
*
* Wolf.soundOff(); //Im a mammal!! that growls
*
* var myWolf = new Wolf();
* myWolf instanceof Mammal //true
* myWolf instanceof Wolf //true
*
* ```
*
* You can also extend a class by using the declare method and just pass in the super class.
*
* ```
* //Typical hierarchical inheritance
* // Mammal->Wolf->Dog
* var Dog = declare(Wolf, {
* instance: {
* constructor: function(options) {
* options = options || {};
* this._super(arguments);
* //override Wolfs initialization of sound to woof.
* this._sound = "woof";
*
* },
*
* speak : function() {
* //Should return "A mammal of type mammal sounds like a growl thats domesticated"
* return this._super(arguments) + " thats domesticated";
* }
* },
*
* static : {
* soundOff : function() {
* //should return "I'm a mammal!! that growls but now barks"
* return this._super(arguments) + " but now barks";
* }
* }
* });
*
* Dog.soundOff(); //Im a mammal!! that growls but now barks
*
* var myDog = new Dog();
* myDog instanceof Mammal //true
* myDog instanceof Wolf //true
* myDog instanceof Dog //true
*
*
* //Notice you still get the extend method.
*
* // Mammal->Wolf->Dog->Breed
* var Breed = Dog.extend({
* instance: {
*
* //initialize outside of constructor
* _pitch : "high",
*
* constructor: function(options) {
* options = options || {};
* this._super(arguments);
* this.breed = options.breed || "lab";
* },
*
* speak : function() {
* //Should return "A mammal of type mammal sounds like a
* //growl thats domesticated with a high pitch!"
* return this._super(arguments) + " with a " + this._pitch + " pitch!";
* },
*
* getters : {
* pitch : function() {
* return this._pitch;
* }
* }
* },
*
* static : {
* soundOff : function() {
* //should return "I'M A MAMMAL!! THAT GROWLS BUT NOW BARKS!"
* return this._super(arguments).toUpperCase() + "!";
* }
* }
* });
*
*
* Breed.soundOff()//"IM A MAMMAL!! THAT GROWLS BUT NOW BARKS!"
*
* var myBreed = new Breed({color : "gold", type : "lab"}),
* myBreed instanceof Dog //true
* myBreed instanceof Wolf //true
* myBreed instanceof Mammal //true
* myBreed.speak() //"A mammal of type lab sounds like a woof thats domesticated with a high pitch!"
* myBreed.get("type") //"lab"
* myBreed.get("color") //"gold"
* myBreed.get("sound")" //"woof"
* ```
*
* ###Multiple Inheritance / Mixins
*
* declare also allows the use of multiple super classes.
* This is useful if you have generic classes that provide functionality but shouldnt be used on their own.
*
* Lets declare a mixin that allows us to watch for property changes.
*
* ```
* //Notice that we set up the functions outside of declare because we can reuse them
*
* function _set(prop, val) {
* //get the old value
* var oldVal = this.get(prop);
* //call super to actually set the property
* var ret = this._super(arguments);
* //call our handlers
* this.__callHandlers(prop, oldVal, val);
* return ret;
* }
*
* function _callHandlers(prop, oldVal, newVal) {
* //get our handlers for the property
* var handlers = this.__watchers[prop], l;
* //if the handlers exist and their length does not equal 0 then we call loop through them
* if (handlers && (l = handlers.length) !== 0) {
* for (var i = 0; i < l; i++) {
* //call the handler
* handlers[i].call(null, prop, oldVal, newVal);
* }
* }
* }
*
*
* //the watch function
* function _watch(prop, handler) {
* if ("function" !== typeof handler) {
* //if its not a function then its an invalid handler
* throw new TypeError("Invalid handler.");
* }
* if (!this.__watchers[prop]) {
* //create the watchers if it doesnt exist
* this.__watchers[prop] = [handler];
* } else {
* //otherwise just add it to the handlers array
* this.__watchers[prop].push(handler);
* }
* }
*
* function _unwatch(prop, handler) {
* if ("function" !== typeof handler) {
* throw new TypeError("Invalid handler.");
* }
* var handlers = this.__watchers[prop], index;
* if (handlers && (index = handlers.indexOf(handler)) !== -1) {
* //remove the handler if it is found
* handlers.splice(index, 1);
* }
* }
*
* declare({
* instance:{
* constructor:function () {
* this._super(arguments);
* //set up our watchers
* this.__watchers = {};
* },
*
* //override the default set function so we can watch values
* "set":_set,
* //set up our callhandlers function
* __callHandlers:_callHandlers,
* //add the watch function
* watch:_watch,
* //add the unwatch function
* unwatch:_unwatch
* },
*
* "static":{
*
* init:function () {
* this._super(arguments);
* this.__watchers = {};
* },
* //override the default set function so we can watch values
* "set":_set,
* //set our callHandlers function
* __callHandlers:_callHandlers,
* //add the watch
* watch:_watch,
* //add the unwatch function
* unwatch:_unwatch
* }
* })
*
* ```
*
* Now lets use the mixin
*
* ```
* var WatchDog = declare([Dog, WatchMixin]);
*
* var watchDog = new WatchDog();
* //create our handler
* function watch(id, oldVal, newVal) {
* console.log("watchdog's %s was %s, now %s", id, oldVal, newVal);
* }
*
* //watch for property changes
* watchDog.watch("type", watch);
* watchDog.watch("color", watch);
* watchDog.watch("sound", watch);
*
* //now set the properties each handler will be called
* watchDog.set("type", "newDog");
* watchDog.set("color", "newColor");
* watchDog.set("sound", "newSound");
*
*
* //unwatch the property changes
* watchDog.unwatch("type", watch);
* watchDog.unwatch("color", watch);
* watchDog.unwatch("sound", watch);
*
* //no handlers will be called this time
* watchDog.set("type", "newDog");
* watchDog.set("color", "newColor");
* watchDog.set("sound", "newSound");
*
*
* ```
*
* ###Accessing static methods and properties witin an instance.
*
* To access static properties on an instance use the `_static` property which is a reference to your constructor.
*
* For example if your in your constructor and you want to have configurable default values.
*
* ```
* consturctor : function constructor(opts){
* this.opts = opts || {};
* this._type = opts.type || this._static.DEFAULT_TYPE;
* }
* ```
*
*
*
* ###Creating a new instance of within an instance.
*
* Often times you want to create a new instance of an object within an instance. If your subclassed however you cannot return a new instance of the parent class as it will not be the right sub class. `declare` provides a way around this by setting the `_static` property on each isntance of the class.
*
* Lets add a reproduce method `Mammal`
*
* ```
* reproduce : function(options){
* return new this._static(options);
* }
* ```
*
* Now in each subclass you can call reproduce and get the proper type.
*
* ```
* var myDog = new Dog();
* var myDogsChild = myDog.reproduce();
*
* myDogsChild instanceof Dog; //true
* ```
*
* ###Using the `as`
*
* `declare` also provides an `as` method which allows you to add your class to an object or if your using node.js you can pass in `module` and the class will be exported as the module.
*
* ```
* var animals = {};
*
* Mammal.as(animals, "Dog");
* Wolf.as(animals, "Wolf");
* Dog.as(animals, "Dog");
* Breed.as(animals, "Breed");
*
* var myDog = new animals.Dog();
*
* ```
*
* Or in node
*
* ```
* Mammal.as(exports, "Dog");
* Wolf.as(exports, "Wolf");
* Dog.as(exports, "Dog");
* Breed.as(exports, "Breed");
*
* ```
*
* To export a class as the `module` in node
*
* ```
* Mammal.as(module);
* ```
*
*
*/
function createDeclared() {
var arraySlice = Array.prototype.slice, classCounter = 0, Base, forceNew = new Function();
var SUPER_REGEXP = /(super)/g;
function argsToArray(args, slice) {
slice = slice || 0;
return arraySlice.call(args, slice);
}
function isArray(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
function isObject(obj) {
var undef;
return obj !== null && obj !== undef && typeof obj === "object";
}
function isHash(obj) {
var ret = isObject(obj);
return ret && obj.constructor === Object;
}
var isArguments = function _isArguments(object) {
return Object.prototype.toString.call(object) === '[object Arguments]';
};
if (!isArguments(arguments)) {
isArguments = function _isArguments(obj) {
return !!(obj && obj.hasOwnProperty("callee"));
};
}
function indexOf(arr, item) {
if (arr && arr.length) {
for (var i = 0, l = arr.length; i < l; i++) {
if (arr[i] === item) {
return i;
}
}
}
return -1;
}
function merge(target, source, exclude) {
var name, s;
for (name in source) {
if (source.hasOwnProperty(name) && indexOf(exclude, name) === -1) {
s = source[name];
if (!(name in target) || (target[name] !== s)) {
target[name] = s;
}
}
}
return target;
}
function callSuper(args, a) {
var meta = this.__meta,
supers = meta.supers,
l = supers.length, superMeta = meta.superMeta, pos = superMeta.pos;
if (l > pos) {
args = !args ? [] : (!isArguments(args) && !isArray(args)) ? [args] : args;
var name = superMeta.name, f = superMeta.f, m;
do {
m = supers[pos][name];
if ("function" === typeof m && (m = m._f || m) !== f) {
superMeta.pos = 1 + pos;
return m.apply(this, args);
}
} while (l > ++pos);
}
return null;
}
function getSuper() {
var meta = this.__meta,
supers = meta.supers,
l = supers.length, superMeta = meta.superMeta, pos = superMeta.pos;
if (l > pos) {
var name = superMeta.name, f = superMeta.f, m;
do {
m = supers[pos][name];
if ("function" === typeof m && (m = m._f || m) !== f) {
superMeta.pos = 1 + pos;
return m.bind(this);
}
} while (l > ++pos);
}
return null;
}
function getter(name) {
var getters = this.__getters__;
if (getters.hasOwnProperty(name)) {
return getters[name].apply(this);
} else {
return this[name];
}
}
function setter(name, val) {
var setters = this.__setters__;
if (isHash(name)) {
for (var i in name) {
var prop = name[i];
if (setters.hasOwnProperty(i)) {
setters[name].call(this, prop);
} else {
this[i] = prop;
}
}
} else {
if (setters.hasOwnProperty(name)) {
return setters[name].apply(this, argsToArray(arguments, 1));
} else {
return this[name] = val;
}
}
}
function defaultFunction() {
var meta = this.__meta || {},
supers = meta.supers,
l = supers.length, superMeta = meta.superMeta, pos = superMeta.pos;
if (l > pos) {
var name = superMeta.name, f = superMeta.f, m;
do {
m = supers[pos][name];
if ("function" === typeof m && (m = m._f || m) !== f) {
superMeta.pos = 1 + pos;
return m.apply(this, arguments);
}
} while (l > ++pos);
}
return null;
}
function functionWrapper(f, name) {
if (f.toString().match(SUPER_REGEXP)) {
var wrapper = function wrapper() {
var ret, meta = this.__meta || {};
var orig = meta.superMeta;
meta.superMeta = {f: f, pos: 0, name: name};
switch (arguments.length) {
case 0:
ret = f.call(this);
break;
case 1:
ret = f.call(this, arguments[0]);
break;
case 2:
ret = f.call(this, arguments[0], arguments[1]);
break;
case 3:
ret = f.call(this, arguments[0], arguments[1], arguments[2]);
break;
default:
ret = f.apply(this, arguments);
}
meta.superMeta = orig;
return ret;
};
wrapper._f = f;
return wrapper;
} else {
f._f = f;
return f;
}
}
function defineMixinProps(child, proto) {
var operations = proto.setters || {}, __setters = child.__setters__, __getters = child.__getters__;
for (var i in operations) {
if (!__setters.hasOwnProperty(i)) { //make sure that the setter isnt already there
__setters[i] = operations[i];
}
}
operations = proto.getters || {};
for (i in operations) {
if (!__getters.hasOwnProperty(i)) { //make sure that the setter isnt already there
__getters[i] = operations[i];
}
}
for (var j in proto) {
if (j !== "getters" && j !== "setters") {
var p = proto[j];
if ("function" === typeof p) {
if (!child.hasOwnProperty(j)) {
child[j] = functionWrapper(defaultFunction, j);
}
} else {
child[j] = p;
}
}
}
}
function mixin() {
var args = argsToArray(arguments), l = args.length;
var child = this.prototype;
var childMeta = child.__meta, thisMeta = this.__meta, bases = child.__meta.bases, staticBases = bases.slice(),
staticSupers = thisMeta.supers || [], supers = childMeta.supers || [];
for (var i = 0; i < l; i++) {
var m = args[i], mProto = m.prototype;
var protoMeta = mProto.__meta, meta = m.__meta;
!protoMeta && (protoMeta = (mProto.__meta = {proto: mProto || {}}));
!meta && (meta = (m.__meta = {proto: m.__proto__ || {}}));
defineMixinProps(child, protoMeta.proto || {});
defineMixinProps(this, meta.proto || {});
//copy the bases for static,
mixinSupers(m.prototype, supers, bases);
mixinSupers(m, staticSupers, staticBases);
}
return this;
}
function mixinSupers(sup, arr, bases) {
var meta = sup.__meta;
!meta && (meta = (sup.__meta = {}));
var unique = sup.__meta.unique;
!unique && (meta.unique = "declare" + ++classCounter);
//check it we already have this super mixed into our prototype chain
//if true then we have already looped their supers!
if (indexOf(bases, unique) === -1) {
//add their id to our bases
bases.push(unique);
var supers = sup.__meta.supers || [], i = supers.length - 1 || 0;
while (i >= 0) {
mixinSupers(supers[i--], arr, bases);
}
arr.unshift(sup);
}
}
function defineProps(child, proto) {
var operations = proto.setters,
__setters = child.__setters__,
__getters = child.__getters__;
if (operations) {
for (var i in operations) {
__setters[i] = operations[i];
}
}
operations = proto.getters || {};
if (operations) {
for (i in operations) {
__getters[i] = operations[i];
}
}
for (i in proto) {
if (i != "getters" && i != "setters") {
var f = proto[i];
if ("function" === typeof f) {
var meta = f.__meta || {};
if (!meta.isConstructor) {
child[i] = functionWrapper(f, i);
} else {
child[i] = f;
}
} else {
child[i] = f;
}
}
}
}
function _export(obj, name) {
if (obj && name) {
obj[name] = this;
} else {
obj.exports = obj = this;
}
return this;
}
function extend(proto) {
return declare(this, proto);
}
function getNew(ctor) {
// create object with correct prototype using a do-nothing
// constructor
forceNew.prototype = ctor.prototype;
var t = new forceNew();
forceNew.prototype = null; // clean up
return t;
}
function __declare(child, sup, proto) {
var childProto = {}, supers = [];
var unique = "declare" + ++classCounter, bases = [], staticBases = [];
var instanceSupers = [], staticSupers = [];
var meta = {
supers: instanceSupers,
unique: unique,
bases: bases,
superMeta: {
f: null,
pos: 0,
name: null
}
};
var childMeta = {
supers: staticSupers,
unique: unique,
bases: staticBases,
isConstructor: true,
superMeta: {
f: null,
pos: 0,
name: null
}
};
if (isHash(sup) && !proto) {
proto = sup;
sup = Base;
}
if ("function" === typeof sup || isArray(sup)) {
supers = isArray(sup) ? sup : [sup];
sup = supers.shift();
child.__meta = childMeta;
childProto = getNew(sup);
childProto.__meta = meta;
childProto.__getters__ = merge({}, childProto.__getters__ || {});
childProto.__setters__ = merge({}, childProto.__setters__ || {});
child.__getters__ = merge({}, child.__getters__ || {});
child.__setters__ = merge({}, child.__setters__ || {});
mixinSupers(sup.prototype, instanceSupers, bases);
mixinSupers(sup, staticSupers, staticBases);
} else {
child.__meta = childMeta;
childProto.__meta = meta;
childProto.__getters__ = childProto.__getters__ || {};
childProto.__setters__ = childProto.__setters__ || {};
child.__getters__ = child.__getters__ || {};
child.__setters__ = child.__setters__ || {};
}
child.prototype = childProto;
if (proto) {
var instance = meta.proto = proto.instance || {};
var stat = childMeta.proto = proto.static || {};
stat.init = stat.init || defaultFunction;
defineProps(childProto, instance);
defineProps(child, stat);
if (!instance.hasOwnProperty("constructor")) {
childProto.constructor = instance.constructor = functionWrapper(defaultFunction, "constructor");
} else {
childProto.constructor = functionWrapper(instance.constructor, "constructor");
}
} else {
meta.proto = {};
childMeta.proto = {};
child.init = functionWrapper(defaultFunction, "init");
childProto.constructor = functionWrapper(defaultFunction, "constructor");
}
if (supers.length) {
mixin.apply(child, supers);
}
if (sup) {
//do this so we mixin our super methods directly but do not ov
merge(child, merge(merge({}, sup), child));
}
childProto._super = child._super = callSuper;
childProto._getSuper = child._getSuper = getSuper;
childProto._static = child;
}
function declare(sup, proto) {
function declared() {
switch (arguments.length) {
case 0:
this.constructor.call(this);
break;
case 1:
this.constructor.call(this, arguments[0]);
break;
case 2:
this.constructor.call(this, arguments[0], arguments[1]);
break;
case 3:
this.constructor.call(this, arguments[0], arguments[1], arguments[2]);
break;
default:
this.constructor.apply(this, arguments);
}
}
__declare(declared, sup, proto);
return declared.init() || declared;
}
function singleton(sup, proto) {
var retInstance;
function declaredSingleton() {
if (!retInstance) {
this.constructor.apply(this, arguments);
retInstance = this;
}
return retInstance;
}
__declare(declaredSingleton, sup, proto);
return declaredSingleton.init() || declaredSingleton;
}
Base = declare({
instance: {
"get": getter,
"set": setter
},
"static": {
"get": getter,
"set": setter,
mixin: mixin,
extend: extend,
as: _export
}
});
declare.singleton = singleton;
return declare;
}
if ("undefined" !== typeof exports) {
if ("undefined" !== typeof module && module.exports) {
module.exports = createDeclared();
}
} else if ("function" === typeof define && define.amd) {
define(createDeclared);
} else {
this.declare = createDeclared();
}
}());