elemantics / joii

Javascript Object Inheritance Implementation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

#JOII# ###Javascript Object Inheritance Implementation###


I've seen lots of attempts from people trying to create a way of implementing object inheritance in javascript, but rarely see anything successful, unless it's bloated with an entire framework. If you're looking for a minimalistic approach, you came to the right place.

See extended documentation and samples here: http://harold.info/projects/joii or on the github page: http://haroldiedema.github.io/joii/

Features

  • Unit-tested using QUnit (http://qunitjs.com/)
  • Lightweight! (3.5k minified)
  • Supports Internet Explorer 6 !
  • Extending native and JOII-classes.
  • Overrides & parent inheritance
  • Using interfaces, making sure a class contains the functionality you need.
  • Using traits, get rid of horizontal code duplication.
  • Constructors & Destructors
  • Aliases for global accessibility.
  • On-the-fly mixin support

Check out the examples below for the possibilities.

Inheritance

Example 1: Simple inheritance

Lets start by declaring a simple "base" class. This could be anything, ranging from a simple function to something large like the WebGL renderer of THREE.js ;)

var BaseClass = function() 
{
    this.some_var = "foobar";
    
    /**
     * @return string
     */
    this.someMethod = function() {
        return this.some_var;
    }
};

Assuming you have basic knowledge of javascript, or any programming language in that matter, the code above should be simple enough to understand.

Lets move on to a class declaration that's going to extend on this class.

var MyClass = Class({ extends: BaseClass }, function()
{
    /**
     * @param string value
     */
    this.__construct = function(value)
    {
        this.some_var = value;
    }
    
})

As soon as the class in this example is going to be instantiated, it'll overwrite the property some_var with the value you're going to give it. If you then execute the method someMethod, it'll return the new value instead of "foobar".

Here's how:

var example = new MyClass("Another value!");
console.log( example.someMethod() );

Easy stuff right?

Example 2: Nested inheritance

Lets move on to something a little more intermediate. We'll create 2 base classes, and 1 normal class. The second base class is going to override an existing method, but still use the functionality from its parent.

// Note that the parameters argument isn't required if you don't need it.
var BaseClass = Class(function()
{
    /**
     * @return string
     */
    this.getText = function()
    {
        return "This is a text which we'll display completely capitalized.";
    }
});

var SecondBaseClass = Class({ extends: BaseClass }, function()
{
    /**
     * @see BaseClass.getText
     */
    this.getText = function()
    {
        return this.parent.getText().toUpperCase();
    }
});

var MyClass = Class({ extends: SecondBaseClass }, function()
{
    this.__construct = function()
    {
        // Prints: THIS IS A TEXT WHICH WE'LL DISPLAY COMPLETELY CAPITALIZED.
        console.log( this.getText() );
    }
})

// instantiate the class.
new MyClass();

Example 3: Using aliases

Sometimes functions (classes) aren't available on the global scope. For this we have aliases. Simply name your class in the parameters object and you'll be able to use this name to extend, implement or mixin on the desired class.

new Interface({ name: 'example.interface' }, {
    foo: 'string'
});

Class({ name: 'example.some.class', implements: ['example.interface'] }, function() {
    this.foo = "bar";
});

Class({ name: 'example.rawr', extends: 'example.some.class' }, function() {
    this.__construct = function() {
        console.log(this.foo) // "bar"
    }
});

var obj = Class({ extends: 'example.rawr' }, function(){});

new obj(); // prints "bar".

Example 4: Use objects instead of functions

If you don't like to use functions as class objects, you're free to use object-style syntax instead:

var BaseClass = Class({
    foo: "bar"
});

var MyClass = Class({ extends: BaseClass }, {
{
    /**
     * @param string value
     */
    __construct: function(value)
    {
        this.some_var = value;
        console.log(this.foo);
    }
});

var m = new MyClass(); // prints: bar

Mixins

If you want to apply functionality from another object on-the-fly, after you already instantiated a class, you can use the .mixin method. This will copy all methods and properties from the mixin to the current object scope. Existing properties and methods are not overwritten unless you give the overwrite argument. See example:

var MyClass = Class(function() {
   this.test = function() {
       return "Hello World";
   }
});
var MyMixin = Class(function() {
   this.test = function() {
       return "Foobar";
   }
});

var m = new MyClass();
console.log(m.test()); // Prints: Hello World

m.mixin(MyMixin);
console.log(m.test()); // Prints: Hello World

// Overwrite existing elements:
m.mixin(MyMixin, true);
console.log(m.test()); // Prints: Foobar

Interfaces

Ever wanted to make sure a certain object contains functionality that you need? JOII gives you the possibility to implement so-called "interfaces". An interface in this way is only an object containing a set of properties that the class must implement. If it does not, an exception is thrown. Once an object implements a certain object, simply use the implements() method to check if it has the functionality you seek.

Declaring an interface is remarkably similar to declaring classes with a few exceptions.

  • An interface can NOT implement other interfaces
  • An interface can NOT use traits
  • An interface CAN extend on other interfaces

Since an interface must never contain any functional code but only a definition of methods which a class must implement, its prefered to use an object-syntax for declaring interfaces. This way the key is the method and the value is the type.

var iTest = new Interface({
    myMethod: 'function'
});

var iAnotherTest = new Interface({ extends: iTest }, {
    anotherMethod: 'function'
})

If you prefer to use an object-syntax for interface decarations or if your company specifies this in their code-style rules, you may also do that:

var iTest = new Interface(function() {
    this.myMethod = 'function';
});

var iAnotherTest = new Interface({ extends: iTest }, function(){
    this.anotherMethod = 'function';
})

Example 1: A simple interface usecase

var iUser = new Interface({
    getUsername: 'function'
});

var iLoggable = new Interface({
    log: function(msg) {}
});

var MyClass = Class({ implements: [iUser]}, function()
{
    this.getUsername = function() {
        return 'user';
    }    
});

var example = new MyClass();

example.implements(iUser); // true
example.implements(iLoggable); // false


if (example.implements(iUser)) {
    console.log( example.getUsername() ); // prints: user
}

Example 2: Interfaces extending interfaces

In some cases, you wish to check if a certain class implements a specific interface. However, this interface you want to check is the parent if the interface your class is using... No problemo!

/**
 * Our base interface.
 */
var iBase = new Interface({
    baseMethod: 'function'
});

/**
 * Our interface we're going to implement, extending on iBase.
 */
var iAnother = new Interface({ extends: iBase}, {
    anotherMethod: 'function'
})

/**
 * Our class. Note that we're only implementing iAnother.
 */
var MyClass = Class({ implements: [iAnother] }, function(){
    
    this.baseMethod = function() {
        return 'This is a base method.';
    }
    
    this.anotherMethod = function() {
        return 'This is another method.';
    }
})

// Instantiate our class.
var my = new MyClass();

// Check for implementations.
my.implements(iAnother); // true
my.implements(iBase); // true

Traits

JOII allows you to implement traits. A trait is simply an object which will have its functionality copied into the designated class. The difference between extending a class or using it as a trait it basically the way that the trait is stateless. Think of copy-pasting code from one class to another, so you don't have to create code duplication across your project.

Example: a simple way of using traits

var tLogger = {    
    log   : function(msg) { console.log(msg); },
    warn  : function(msg) { console.warn(msg); },
    error : function(msg) { console.error(msg); }
}
var tMath = {
    add : function(a, b) { return a + b; },
    sub : function(a, b) { return a - b; }
}

var MyClass = Class({ uses: [tLogger, tMath] }, function()
{
    this.__construct = function()
    {
        this.log('2 + 5 = ' + this.add(2, 5));
    }
});

Everything combined

Lets combine everything we've been going through so far into one working example. How about we create some sort of object storage in combination with a user object.

We're going to start by defining some base class on which we'll extend our functionality.

var ObjectStorage = function()
{
    this.storage = {};
    
    this.set = function(key, value) {
        this.storage[key] = value;
    }
    
    this.get = function(key, _default) {
        return this.storage[key] || _default;
    }
}

Moving on to some interfaces. Afterall, we want to make sure we're dealing with an actual "user" object that contains the functionality we eventually need...

var iUser = new Interface({
    getUsername    : function(){},
    getDisplayname : function(){}
});

var iLoggable = new Interface({
    log : function(msg){}
});

And a simple logger trait that lets us...erm... log things.

var tLogger = {
    log: function(msg) {
        console.log(msg);
    }
};

And the final product.

  • We extend on ObjectStorage.
  • We implement the interface definition of iUser and iLoggable.
  • We use the functionality of the trait tLogger.

Note that once binding the interface iLoggable to this class, the definition rule is directly solved because we're injecting the required functionality using the trait tLogger.

var User = Class({extends: ObjectStorage, implements: [iUser, iLoggable], uses: [tLogger]}, function() {

    this.__construct = function(username, displayname)
    {
        this.set('username', username);
        this.set('displayname', displayname);
        this.log('User class constructed for ' + username);
    }
    
    this.__destruct = function() {
        this.log('User object ' + username + ' destructed.');
    }

   this.getUsername = function()
   {
       this.log('Requested username');
       return this.get('username');
   }
   
   this.getDisplayname = function()
   {
       this.log('Requested displayname');
       return this.get('displayname');
   }
   
});

Final usage:

var person = new User('harold', 'Harold Iedema');

if (person.implements(iUser) && person.implements(iLoggable)) {
    person.log(person.getDisplayname());
}

About

Javascript Object Inheritance Implementation

License:MIT License