thomasfrank.se

Add some klass to JavaScript

August 14, 2011

In our team of developers at work we sometimes discuss the elusive nature of JavaScript when it comes to classes, inheritance and making object members protected/private. The problem is not finding a way of doing this - it is finding one consistent way of doing so, for future maintainability.
JavaScript gives you many options: Object literals, prototypical inheritance, different libraries for emulation of classical inheritance, closures created in anonymous functions to emulate protected/private variables -- you name it. And yes: There are already several good libraries out there with different approaches to all or some of this (amongst them Base, JS.Class and Cobra).
The reason I decided to write yet another library was that I wished to provide a tool that lets us work with a syntax close to the traditional closure encapsulation style of writing singletons, but with the added benefits of instantiation, public, protected, private, abstract and final methods, multiple inheritance, calls to super methods etc. I named it "klass" (the Swedish word for "class"). Download klass.js (full source code, 18 kb) or klass-min.js (compressed and gzipped, 3.4 kb).
How do you use klass? Start off by browsing the cheat sheet and/or by botanizing among the examples below.

Cheat sheet -- klass.js

klass(function(){
  ...
})
creates a class definition
-- all variables you declare within it will be
protected by default


Within the definition, preferably at the beginning, you may call different functions. Their inparameters are always references to variables and functions within the definition:

_extends (classRef1, etc...)inherit from one or more classes _public (method1, etc...)make methods public _public (_g.variable1, etc...)create public getter methods _public (_gs.variable1, etc...)create public getter/setter methods _private (member1, etc...) make instance members private _abstract () make the class abstract _abstract (method1, etc...)make the class & selected methods abstract _final (method1, etc...)make methods final _singleton ()create a singleton instance _singleton (false)extend a singleton into a non-singleton


Inside your methods:
_super (arguments...)reference to the overridden parent method


Magic methods you may define within your class definition:
function _constructor (args...){...}called on instance creation function _static (){...}a class definition for static members

A simple example -- using the extends, public, super and constructor features

Let's start off with a simple example of inheritance, constructors, making methods public and calling super methods. In this example Animal is our base class. Snake extends/inherits from Animal, as do Horse.
Run this example
// Class definitions
var Animal = klass (function Animal(){
  _public (move, bite);
  var name;
  var _constructor = function(n){
    name = n
  };
  var move = function(meters){
    alert (name + ' moved ' + meters + 'm.')
  };
  var bite = function(){
    alert (name + ' bites!');
  };
});

var Snake = klass (function Snake(){
  _extends (Animal);
  var move = function(){
    alert ('Slithering');
    _super (5);
  };
  var bite = function(){
    alert(name + ' attacks with a venomous bite!');
   };
});

var Horse = klass (function Horse(){
  _extends (Animal);
  var move = function(){
    alert ('Galloping');
    _super (45);
  };
});

// Create some instances and call their methods
var sam = new Snake ('Sammy the Python');
var tom = new Horse ('Tommy the Palomino');
sam.move();
sam.bite();
tom.move();
tom.bite();

Some ground rules

The features examined above might be the only ones you use in the klass library. That's just fine! However, because of how I've designed the klass library, you should try to follow three important ground rules:
  1. All code in a class definition should reside inside the methods you define, except for variable declarations and calls to the special klass methods.
  2. All members are protected by default. Visibility and mutability can only be changed for methods (using calls to _public, _abstract and _final), not other members/variables. The exception to this rule is that _private can be used for all members (methods and variables).
  3. Do not define variables/methods that conflict with the ones klass use (_public, _abstract etc.) in variable scopes that surround your class definitions. The exception is the global scope where you are free to do so.

Also note: Although not necessary you might benefit from naming your class definition functions, that is someClass = klass (function someClass(){...}) is preferable over someClass = klass (function (){...}), since this makes debugging easier in Chrome -- when you write an instance to console.log you will then see the class name.

Creating getters and setters

Our second ground rule states that we can only make methods public. However you can easily create getters or getter/setters for other members/variables. You do this by prefixing the member name with either _g.methodName (getter) or _gs.methodName (getter/setter) in your call to _public:
Run this example
// Class definition
var Animal = klass (function Animal(){
  _public (_g.brain,_gs.legs); 
  var legs = 4;
  var brain = 'hopefully';
});

// Setting values (since brain is "get-only" it will not change)
var myAnimal = new Animal();
myAnimal.legs (3);
myAnimal.brain ('not really');

// Getting values
alert ('legs: ' + myAnimal.legs())
alert ('brain: ' + myAnimal.brain());

Singletons

You can create singletons, by calling _singleton (). In this case klass will return a single instance of the object directly, instead of the class constructor.
A child class of a singleton can be made a non-singleton, by calling _singleton (false):
Run this example
// Class definitions
var Universe = klass (function Universe(){
  _singleton ();
  _public (_g.size);
  var size = 'large';
});

var ParallellUniverse = klass (function ParallellUniverse(){
  _extends (Universe);
  _singleton (false);
  var size = 'unknown';
});

// Universe becomes an instance directly since it is a singleton
alert (Universe.size());

// But our parallell universes are not singletons and require instantiation
var pu1 = new ParallellUniverse ();
var pu2 = new ParallellUniverse ();
alert (pu1.size ());
alert (pu2.size ());

Private members

Define which members that should be private by calling _private. Private members do not get inherited in subclasses.
Run this example
// Class definitions
var Wolf = klass (function Wolf(){
  _public (bite);
  _private (howl, kill);
  var species = "Wolf";
  var kill = "I kill what I've bitten.";
  var howl = function(){
    alert ("Aoooooo!");
  };
  var bite = function(){
    alert (
      "I'm a " + species 
      + "!\nI bite!\n" 
      + kill
    );
    howl ();		
  };
});

var Dog = klass (function Dog(){
  _extends (Wolf);
  var species = "Dog";
});

// Create a wolf and a dog, then let them bite
// the Dog will not now the value of "kill" or "howl"
// so an error will be thrown when it tries to howl
var bigBad = new Wolf();
var smallPup = new Dog();
bigBad.bite();
smallPup.bite();

Abstract classes and methods

Abstract classes can be seen as templates for child classes. You make a class abstract by calling _abstract (). Instances can not be created from an abstract class.
If you specify abstract methods as well using abstract (methodName1, methodName2) they all have to be overridden in the child class, before you can create any instances.
Run this example
// Class definitions
var Animal = klass (function Animal(){
  _abstract ();
});

// Trying to create an instance of Animal - throws an error since Animal is abstract
var myAnimal = new Animal();
Run this example
// Class definitions
var Animal = klass (function Animal(){
  _public (legs);
  _abstract (legs);
});

var Snake = klass (function Snake(){
  _extends (Animal);
  var legs = function(){
    alert ("I'm a snake - I don't have any legs");
  };
});

var Dog = klass (function Dog(){
  _extends (Animal);
});

// Creating an instance of Snake
var mySnake = new Snake ();
mySnake.legs ();

// Trying to create an instance of Dog - throws an error since legs is abstract
var myDog = new Dog ();

Final methods

Making a method final means that it can not be overridden in child classes.
Run this example
// Class definitions
var Browser = klass (function Browser(){
  _public (status);
  _final (status);
  var status = function(browserName){
    return browserName == 'ie6' ? 'dead' : 'alive';
  };
});

var MsBrowser = klass (function MsBrowser(){
  _extends (Browser);
  // trying to override status - this will throw an error since status is final
  var status = function(){
    return 'alive'
  }
});

Static members

Static members are shared between all instances of a class. You define them by defining the magic method _static in your class definition. The code inside _static is written just like a normal klass definition.
If you make a static method public, it gets attached to the class constructor.
Run this example
//Class definition
var Animal = klass (function Animal(){
  _public (info, galacticMove);
  var _static = function(){
    _public (generalStatus);
    var generalStatus = function(){return 'amazing'};
    var planet = 'Earth';  
  };
  var name;
  var _constructor = function (n){name = n};
  var info = function(){
    alert ("Hi there I'm " + name + ' and I live on ' + planet + '...');
  };
  var galacticMove = function (newPlanet){
    alert ("We can't move just " + name + " to " + newPlanet + "."
      + "\nLet's move ALL animals there...");
    planet = newPlanet;
  };
});

// Let's create two fine animals
var cam = new Animal ('Cammy The Cobra');
var liv = new Animal ('Liv The Lion');
// Now Cammy the Cobra moves to Mars
cam.galacticMove ('Mars');
// And as it turns out Liv The Lion lives on Mars too, since planet is a static member
cam.info();
liv.info();
// Public static methods are attached to the class constructor
alert("All animals are " + Animal.generalStatus() + '.');

Multiple inheritance

The klass library lets you handle multiple inheritance by passing an infinite number of parent classes to _extends.
Run this example
// Class definitions
var Plant = klass (function Plant(){
  var typeOfOrganism = 'plant';
  var asexualReproduction = true;
  var stationary = true;
});

var Animal = klass (function Animal(){
  var typeOfOrganism = 'animal';
  var eatsOtherOrganisms = true;
});

var Fungus = klass(function Fungus(){
  _extends (Animal, Plant);
  var typeOfOrganism = 'fungus';
  var _constructor = function(){
    var aboutMe = [
      "I'm a " + typeOfOrganism + ".",
      "I " + (eatsOtherOrganisms ? '' : "don't ") + "eat other organisms.",
      "I think sex is " +  (asexualReproduction ? "overrated." : "important."),
      stationary ? "I don't move a lot..." : "I like to move it, move it!"
    ].join('\n\n');
    alert (aboutMe);
  };
});

// A Fungus gets created and tells us about life
var myFungus = new Fungus();

Alternative syntax -- prefixing function names

You can prefix a method name to determine if it should be public, private, abstract and/or final in the method name. You do this by adding _public, _private, _abstract and/or _final as a prefix to the name of the method. The "real" part of the method name should also be prefixed by an underscore, as seen in the example:
Run this example
// Class definition using alternative syntax
var Animal = klass (function Animal(){
  var name;
  var _constructor = function(n){
    name = n
  };
  var _public_move = function(meters){
    alert (name + ' moved ' + meters + 'm.')
  };
  var _public_final_bite = function(){
    alert (name + ' bites!');
  };
});

// An instance of Animal
var myAnimal = new Animal ('Furry Fido');
myAnimal.move (10);
myAnimal.bite ();


Please consider: Choose one syntax within your developer team, to avoid confusion. (If you wish to you can turn off the alternative syntax via klass.settings).

Settings -- change how klass works and modify the syntax

There are several settings that you can apply to klass in order to change allowed syntax and the level of debuggability.
Run this example
// Default settings for klass
{
  // Function names etc...
  publicFuncname : '_public',
  privateFuncname: '_private',
  extendsFuncname: '_extends',
  singletonFuncname: '_singleton',
  finalFuncname: '_final',
  abstractFuncname: '_abstract',
  staticFuncname: '_static',
  constructorFuncname: '_constructor',
  superFuncname: '_super',
  getterFuncname: '_g',
  getterSetterFuncname: '_gs',
  // If the alternative syntax - using method prefixes - is allowed
  allowAlternativeSyntax: true
};

// Change one or several settings
klass.settings({
  publicFuncname: '_pub',
  getterSetterFuncname: 'getset'
});

// Now this will work - note the use of "_pub" and "getset"
Animal = klass (function Animal(){
  _pub (getset.isAlive);
  var isAlive;
});

var myAnimal = new Animal ();
myAnimal.isAlive ("I'm alive and kicking!");
alert (myAnimal.isAlive());

Working with JavaScript compressors/minifiers

The klass library is slightly picky about how code containing class definitions can be compressed using different JavaScript minifiers. Here are some guidelines to make things work smoothly:
Should you use a minifier not mentioned above -- check its documentation and see if it can be set to not mangle/change variable names. If it can you'll probably be fine.

Performance and stability

I've tested klass in all major browsers and instance creation is rather fast as far as I can see. Memory usage (tested using the profiler in Chrome) is good to. If I get time to time things more thoroughly I might get back to you with some benchmarks.
However we haven't used klass in production yet, and I fully expect some bugs to creep up. If you stumble over one, please report it here.
[comments]