Fork me on GitHub

Capsule.js

Motivation

I switched from traditional OOP Languages to using alot of JavaScript about a year ago and while I love the expressiveness and freedom it gives, I really miss the basic polymorphic goodness you get with other languages. Now obviously you can extend objects and such in JavaScript but it is an eyesore to say the least. There are also libraries to do this for you (underscore, google closure, etc) but they are usually very large and require you to use some type of global object to be a wrapper around the whole process. The point of this library is to make traditional OOP practices native to JavaScript Objects so that you can code with intuitive syntax and have it work how you would expect. And for those of you worried about prototype pollution, all (API) properties added to Object are NON-ENUMERABLE...so 'for (prop in obj)' will not be corrupted. Along with the API bits that deal with inheritance and code reuse I also implemented reactive variables so that you can have handlers called whenever a specific property of an object is changed .Also did I mention it's less than 9kb un-minified and less than 5kb minified?

Download

Usage

Browser:

Simply import Capsule.js via a <script/> tag before any code that uses its API.

<script type="text/javascript" src='path/to/Capsule.js'></script>
<script type="text/javascript" src='path/to/MyApp.js'></script>

Node:

Make sure to require Capsule.js before the first use of its API. You only need to require it once and you dont need to catch it as a variable.

require('./path/to/Capsule.js'); 

API

namespace(nameSpaceStr)

Verifies that the given namespace exists in the global scope and if it doesn't, creates it. Allows you to logically organize the code in an app as well as avoid naming collisions.

  //FILE 1
  namespace('Monsters.Base');

  Monsters.Base.Monster = function(){
    this.evil = true; 
  }

  //FILE 2
  namespace('Monsters.Interfaces');

  Monsters.Interfaces.TheUndead = {
    attack : 'function'
  }

  // FILE 3
  namespace('Monsters.TheUndead');

  Monsters.TheUndead.Vampire = function(){
    this.super();
  }.extends(Monsters.Base.Monster).implements(Monsters.Interfaces.TheUndead);

  Monsters.TheUndead.Vampire.prototype.attack = function(){
    if(this.evil){
      console.log('Chomp');
    }
  }

  //FILE 4
  namespace('Monsters.TheUndead');

  Monsters.TheUndead.Zombie = function(){
    this.super();
  }.extends(Monsters.Base.Monster).implements(Monsters.Interfaces.TheUndead);

  Monsters.TheUndead.Zombie.prototype.attack = function(){
    if(this.evil){
      console.log('Slurp');
    }
  }

  //FILE 5
  var vamp = new Monsters.TheUndead.Vampire(),
  zombie = new Monsters.TheUndead.Zombie();

  vamp.attack(); // Chomp
  zombie.attack(); // Slurp

  //*** FILES 1 & 2 MUST BE LOADED BEFORE FILES 3 & 4 WHICH MUST BE LOADED BEFORE FILE 5 ***

extends(superClass)

Extends a class to have all the functionality of superClass.

  var Vampire = function(){
    this.super();  
  }.extends(Monster);

  var dracula = new Vampire();
  dracula instanceof Monster // true

super(funcName, argArray)

Can be used in two different ways. If funcName is not present, the super gets called with the callers context ('this') and argArray as its parameters. If funcName is present, then the the SuperClass's function given by funcName is called with the argArray as its parameters and the callers 'this' as it's context.

  var Monster = function(diet, homeland){
    this.diet = diet;
    this.homeland = homeland;
    this.intimidate = function(intim){ //can only be overwritten in SubClass's Constructor
        console.log(intim);
    };
  }

  Monster.prototype.hunt = function(beginning, end){
    console.log('I am going hunting from ' + beginning + ' to ' + end);
  }

  var Vampire = function(){
    this.super(['blood', 'transylvania']);  
    this.intimidate = function(){     // defined AFTER super() call
        this.super('intimidate', ['You look tasty']);   
    };
  }.extends(Monster);

  Vampire.prototype.hunt = function(){
    this.super('hunt', ['dusk', 'dawn']);
    console.log("The sun doesn't agree with me");
  }

  var dracula = new Vampire();
  dracula.diet; //blood
  dracula.homeland; //transylvania
  dracula.hunt(); // prints 'I am going hunting from dusk to dawn' then "The sun doesn't agree with me"
  dracula.intimidate(); //prints 'You look tasty'

implements(interfaceObj)

Adds each property of the interfaceObj to an Objects prototype. If any of the properties that are not in the 'abstract' object are referenced before they are explicitly defined or set to a value that != its current value (when used with typeof), then an error is thrown. This is to guarantee that you explicitely define these values to satisfy the interface. Any of the properties in the 'abstract' object of the interface just get added to the prototype and can be used accordingly. This can be chained for multiple interfaces ie. var myClass = function(){}.implements(a).implements(b);.

  var TheUndead = {
    infectionStory : 'string',
    abstract : {
      die : function(){
        console.log('lol too late');
      },
      sobStory : function(){
        console.log('I got this way when ' + this.infectionStory + ' :(')
      }
    }
  }  

  var Vampire = function(){}.implements(TheUndead);

  var dracula = new Vampire();
  dracula.die();//prints 'lol too late'

  dracula.sobStory();//these both throw 'Error: attempting to access unimplemented interface property infectionStory.'
  dracula.infectionStory;//because infectionStory has not been properly set yet

overload(funcStr, implFunc, argTypeArr)

Overloads a function on the callers prototype so that different implementations can be resolved at runtime based on the args passed in.

  var Hero = function(){};

  var Monster = function(){};

  Monster.prototype.growl = function(){ console.log('grrr') };

  Monster.overload('attack', function(){
      console.log('i take no params');
  });

  Monster.overload('attack', function(a){
      console.log('I take a string');
  }, [String]);

  Monster.overload('attack', function(munster){
      munster.growl();
  }, [Monster]);

  Monster.overload('attack', function(a){
      console.log('I take a number');
  }, [Number]);

  Monster.overload('attack', function(a){
      console.log('I take a number, a string, and a Hero');
  }, [Number, String, Hero]);

  var Vampire = function(){
    this.super();
  }.extends(Monster);

  var NewBornVampire = function(){
    this.super();
  }.extends(Vampire);

  var m = new Monster();
  m.attack(); // i take no params
  m.attack('dfgsdf'); // I take a string

  var vamp = new Vampire();
  vamp.attack(1); // I take a number
  vamp.attack(vamp); // grrr

  var newvamp = new NewBornVampire();
  newvamp.attack(2, 'sf', new Hero()); // I take a number, a string, and a Hero
  newvamp.attack(2, null, new Hero()); // I take a number, a string, and a Hero
  newvamp.attack(null); // I take a number
  newvamp.attack({}); // throws 'attack called with the wrong type of parameters.'

reactive(propStr, value, oldSingleton, newSingleton)

Creates a reactive property on the caller initialized to value. The property will then emit an event any time it is changed. if oldSingleton / newSingleton is true, all of the callbacks will recieve the same copy of the old value / new value. If it is false, they will all recieve their own copy.

  var Reactor = function(singleton){
    this.reactive('bomb', 'unstable');
    this.reactive('dud', {
      changMe : 1
    }, false, singleton);
  }

  var kaboom = function(value){
    console.log('KABOOOOM');
  };

  var fizz = function(){
    console.log('Fizzz');
  };

  var fiz = function(oldValue, newValue){
    oldValue.fizzle = 'fiz';
    newValue.fizzle = 'fiz';
  }

  var zle = function(oldValue, newValue){
    newValue.fizzle += 'zle';
    console.log(oldValue);
    console.log(newValue);
  }

  var reactor = new Reactor(),
  sharedReactor = new Reactor(true);

  reactor.arm('bomb', kaboom);
  reactor.arm('dud', fizz);
  sharedReactor.arm('dud', fiz);
  sharedReactor.arm('dud', zle);

  reactor.bomb; //'unstable'
  reactor.bomb = 'about to explode...'; // causes callback to be called...prints 'KABOOOOM'

  reactor.dud; // { changeMe : 1 }
  reactor.dud.changeMe = 2; // nothing happens
  reactor.dud = {}; // causes callback to be called...prints 'Fizzz'

  sharedReactor.dud = {}; // causes fiz() and zle() to be called passing them a singleton
                          // prints { changeMe : 1 } then { fizzle : 'fizzle' }

arm(propStr, callback, context)

Allows you to provide a handler for the event emitted when a reactive property is changed.

  var Reactor = function(){
    this.reactive('bomb', 'unstable');
  }

  var reactor = new Reactor();

  reactor.arm('bomb', function(value){
    console.log(this);
    console.log(value);
  });

  reactor.bomb; //'unstable'
  reactor.bomb = 'about to explode...'; // prints { bomb : 'about to explode...' } then 'about to explode...'

disarm(propStr, callback, optContext)

Removes either a specified handler, or all handlers from a reavtive property

  var Reactor = function(){
    this.reactive('bomb', 'armed');
  };

  var spark = function(){
    console.log('spark');
  };

  var r = new Reactor();

  r.arm('bomb', spark);
  r.arm('bomb', function(){
    console.log('fizzle');
  });
  r.arm('bomb', function(){
    console.log('boom');
  });

  r.bomb = 'exploding'; // causes callbacks to call 'spark' then 'fizzle' then 'boom'
  r.disarm('bomb', spark);

  r.bomb = 'no spark'; //causes callbacks to call 'fizzle then 'boom'
  r.disarm('bomb');

  r.bomb = 'add more handlers to re-arm me!' // nothing happens

clone()

Returns a deep copy of the calling object. Can be used on built in or user defined data types.

  var Monster = function(){
    this.name = 'munster';
    this.diet = {
      breakfast : 'brains',
      lunch     : 'blood',
      dinner    : 'chicken'
    }
  }

  Monster.prototype.yell = function(){
    console.log(this.name.toUpperCase());
  }

  var Vampire = function(){
    this.super();
  }.extends(Monster);

  var dracula = new Vampire(),
  copy = dracula.clone();

  copy.yell() // 'MUNSTER'
  copy.name = "this shouldn't change dracula's name";
  copy.diet.breakfast = 'bacon';

  dracula.yell() // 'MUNSTER'
  dracula.diet.breakfast; // 'brains'

  var original = [1, 2, 3],
  copy = original.clone();

  copy.push(4); // [1, 2, 3, 4]
  original; // [1, 2, 3]

consume(other, mutator, global)

Consumes all the properties in other that already exist in 'this'. Allows you to set default values in your code and have them overridden by an init Object without having to explicitely check for their existence.

  var blade = {
    name : 'blade',
    attack : function(){
      console.log("Oh you're human?...nevermind you are free to go");
    }
  }

  var Monster = function(){};

  Monster.prototype.attack = function(){
    console.log('grrrrrr');
  }

  var Vampire = function(initObj){
    this.super();
    this.name = 'dracula';
    this.preferredMeal = 'blood';
    if(initObj){
      this.consume(initObj); 
    }
  }.extends(Monster);

  var a = new Vampire(blade); //ideal way to use consume (in constructor)
  a.name; // 'blade' 
  a.attack(); //prints 'grrrrrr'

  var b = new Vampire();
  b.consume(blade); //alternate use case (outside of constructor)
  b.name; // 'blade' 
  b.attack(); //prints 'grrrrrr'

  var c = new Vampire();
  c.consume(blade, true); 
  c.name // 'blade' 
  c.attack(); //now prints "Oh you're human?...nevermind you are free to go"

  var d = new Vampire();
  d.consume(blade, function(prop){
    if(typeof prop == 'function'){
      return function(){ 
        console.log('Mwuahaha I overwrote your function');
        prop();
      }
    }
    return 'The Vampire ' + prop;
  }, true); 
  d.name // 'The Vampire blade' 
  d.attack(); // now prints 'Mwuahaha I overwrote your function' and then 
              //"Oh you're human?...nevermind you are free to go" 

projectOnto(other, options)

Places all or a subset, depending on the options object, of an objects properties onto other.

  var Monster = function(){
    this.name = 'Munster';
    this.age  = '212';
  }

  Monster.prototype.eat = function(){
    console.log('nom nom nom');
  }

  var monster = new Monster(),
  cleanObject = {};

  monster.projectOnto(cleanObject); // cleanObject now has the properties 'name', 'age', and 'eat' defined

  monster.projectOnto(cleanObject, {
    filter : function(prop){
      if(!this.hasOwnProperty(prop)){
        return true;
      }
      return false;
    }.bind(cleanObject) //must be bound...otherwise 'this' refers to monster instead of cleanObject
  }); //will result in no change as cleanObject already has all the properties being tested for

  monster.projectOnto(cleanObject, {
    mutator : function(prop){
      if (typeof prop == 'function'){
        return prop;
      }
      return 'Changed value: ' + prop;
    }
  }); //now cleanObject.name == 'Changed value: Munster' and cleanObject.age = 'Changed value: 212'

hasProperty(property)

Similar to hasOwnProperty() except that it searches the whole prototype chain. Basically just syntactic sugar for the in operator

  var Monster = function(){
    this.name = 'Munster';
    this.age  = '212';
  }

  Monster.prototype.attack = function(){};

  var Vampire = function(){
    this.super();
  }.extends(Monster);

  var vamp = new Vampire();
  vamp.hasOwnProperty('name'); //true
  vamp.hasOwnProperty('attack'); //false
  vamp.hasProperty('name'); //true
  vamp.hasProperty('attack'); //true

API Tips

Extended Example

//FILE 1========================================================================
namespace('Monsters.Base');

//Character Base Class-------------------------
Monsters.Base.Character = function(name, evil){
  this.cheesyLine = function(line){ 
    console.log(line);             
  };                                
  this.name = name;                 
  this.health = 100;                
  this.reactive('evil', evil);      
  this.greet();
}

Monsters.Base.Character.prototype.greet = function(){
  console.log(this.name + ' checking in...');
}

Monsters.Base.Character.overload('regenerate', function(health){
  this.health += health;
}, [Number]);

//Monster Base Class-------------------
Monsters.Base.Monster = function(name){
  this.super([name, true])
}.extends(Monsters.Base.Character);

Monsters.Base.Monster.prototype.convert = function(){
  var hero = new Monsters.Base.Hero(this.name);
  return hero;
}

Monsters.Base.Monster.prototype.attack = function(character, att, power){
  if(character instanceof Monsters.Base.Monster){
    console.log('I only eat heroes');
    return;
  }
  console.log(att);
  character.health -= power;
}

Monsters.Base.Monster.overload('regenerate', function(message, health){
  console.log(message);
  this.regenerate(health);
}, [String, Number]);

//Hero Base Class-------------------
Monsters.Base.Hero = function(name){
  this.super([name, false]);
  this.cheesyLine = function(){
    this.super('cheesyLine', ['lose the zero...get with the hero']);
  };
}.extends(Monsters.Base.Character);

Monsters.Base.Hero.prototype.convert = function(){
  return new Monsters.Base.Monster(this.name);
}

//FILE 2=======================================================================
namespace('Monsters.Interfaces');

//TheUndead Interface------------
Monsters.Interfaces.TheUndead = {
  attack    : 'function',
  resurrect : 'function',
  abstract : {
    die : function(){
      console.log('Too late...');
    },
    schedule  : function(){ 
      console.log('I need to feed at least ' + this.hungerLvl + ' times a day');
    },
    hungerLvl : 2
  }
}

// FILE 3======================================================================
namespace('Monsters.TheUndead');

//Vampire Class-----------------------------
Monsters.TheUndead.Vampire = function(name){
  this.super([name]);
  this.strength = 20;
}.extends(Monsters.Base.Monster).implements(Monsters.Interfaces.TheUndead);

Monsters.TheUndead.Vampire.prototype.attack = function(character){
  this.super('attack', [character, 'Chomp', this.strength]);
}

Monsters.TheUndead.Vampire.overload('regenerate', function(){
  this.regenerate('The blood is kicking in...', 20);
});

//Vampire SubClass---------------------------------
Monsters.TheUndead.NewBornVampire = function(name){
  this.super([name]);
  this.strength = 40;
}.extends(Monsters.TheUndead.Vampire);

Monsters.TheUndead.NewBornVampire.prototype.schedule = function(){
  this.super('schedule');
  console.log('give or take 3 or 4 meals');
}

//Zombie Class----------------------------- 
Monsters.TheUndead.Zombie = function(name){
  this.super([name]);
}.extends(Monsters.Base.Monster).implements(Monsters.Interfaces.TheUndead);

Monsters.TheUndead.Zombie.prototype.attack = function(character){
  if(character instanceof Monsters.Base.Monster){
    console.log("I don't care that you are a monster...");
  }
  console.log('Slurp');
  character.health -= 10;
}

//FILE 4=======================================================================

var vamp = new Monsters.TheUndead.Vampire('dracula'); //dracula checking in...
zombie = new Monsters.TheUndead.Zombie('garth'); //garth checking in... 
newBorn = new Monsters.TheUndead.NewBornVampire('newby'); //newby checking in... 
human = new Monsters.Base.Hero('charlie'); //charlie checking in... 

vamp.attack(zombie); // I only eat heroes 
vamp.attack(human); // Chomp

zombie.attack(vamp); // I don't care that you are a monster... Slurp
zombie.attack(human); // Slurp

newBorn.attack(human); // Chomp
newBorn.attack(zombie); // I only eat heroes ***inherited from Vampire***


console.log('human: ' + human.health); // human: 30
console.log('vamp: ' + vamp.health); // vamp: 90
console.log('zombie: ' + zombie.health); // zombie: 100

vamp.regenerate(); // The blood is kicking in...
console.log('vamp: ' + vamp.health); // vamp: 110

zombie.regenerate(10);
console.log('zombie: ' + zombie.health); // zombie: 110

zombie.regenerate(); // throws 'regenerate called with the wrong type of parameters.'

zombie.arm('evil', function(oldVal, newVal){
  console.log('oldval:' + oldVal); // oldval: true
  console.log('newval:' + newVal); // newval: false
  if(newVal == false){
    zombie = zombie.convert();
  }
});

zombie.die(); // Too late...
newBorn.die(); // Too Late... ***inherits abstract method from Vampire implementing TheUndead***

zombie.evil = false; //garth checking in...

console.log(zombie instanceof Monsters.Base.Monster); //false
console.log(zombie instanceof Monsters.Base.Hero); //true
console.log(zombie instanceof Monsters.Base.Character); //true

console.log(zombie.evil); // false
console.log(zombie.health); // 100
vamp.attack(zombie); //Chomp
console.log(zombie.health); // 80

zombie.cheesyLine(); //lose the zero...get with the hero 
newBorn.schedule(); //prints 'I need to feed at least 2 times a day' then 'give or take 3 or 4 meals'
newBorn.resurrect(); // throws Error: attempting to access unimplemented interface property resurrect. 

License

The MIT License (MIT)

Copyright (c) 2013 Charles R. Lipford

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.