defineClass

Simple yet powerful OOP and AOP for JavaScript.

npm install defineClass
4 downloads in the last week
4 downloads in the last month

defineClass

Simple yet powerful OOP for JavaScript with support for traits (mixins)

Defining a class is as simple and declarative as this:

var Person = defineClass({
  constructor: function (firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  },

  greet: function (name) {
    console.log("Hello " + name + ". My name is " + this.firstName);
  } 
});

var p = new Person("John", "Doe");
p.greet("Nick"); // Hello Nick. My name is John
assert(p instanceof Person);

Installation

$ npm install defineClass

Contents:

  1. Defining classes
    1. Default field values
  2. Inheritance
    1. Overriding default field values and methods and calling base methods
  3. Traits
    1. Calling class methods in traits
    2. Overriding base class methods with traits
    3. Trait order in _super
  4. Proxy classes
    1. Method delegation using proxy traits
  5. Nested classes
    1. Decreasing complexity
    2. Nested class overriding
      1. Special notation

Defining classes

Default field values

If a field has an immutable default value, it can be included in the prototype.

var Person = defineClass({
  cash: 0, // define a default value

  constructor: function (firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  },

  greet: function (name) {
    console.log("Hello " + name + ". My name is " + this.firstName);
  },

  earn: function (amount) {
    this.cash += amount;
    return this.cash;
  }
});

var p = new Person("John", "Doe");
assert(this.cash === 0);
p.earn(1000);
assert(this.cash === 1000);

Constructor is optional

Here is the minimal class definition:

var MinimalClass = defineClass({});

Inheritance

A special _super field in a prototype is reserved for inheritance and can reference a base class.

A subclass inherits the members of its base class. It is not required to re-define the constructor in a prototype.

var Boss = defineClass({
  // derive Boss from Person
  _super: Person,

  // a new method
  playGolf: function () {
    console.log("Playing golf...");
  }
});

var boss = new Boss("Robert", "Roe"); // base constructor is called
boss.playGolf(); // Playing golf...

assert(b instanceof Person);
assert(b instanceof Boss);

Overriding default field values and methods and calling base methods

Members in a subclass override the base members of the same name. To call a base method, call the special this._super method. It applies to constructors too.

var Developer = defineClass({
  _super: Person,

  // override a field default value
  cash: 100,

  // override the constructor
  constructor: function (firstName, lastName, language) {
    // you may have code before the base constructor call

    // call the base constructor
    this._super(firstName, lastName);
    this.language = language;
  }

  // override a method
  greet: function (name) {
    console.log("Hey, " + name + ". I'm " + this.firstName)
  },

  // override a method and call its base method
  earn: function (amount) {
    return this._super(amount * 1.2);
  }
});

var dev = new Developer("Jane", "Doe", "Python");
assert(dev.cash === 100);
dev.earn(100);
assert(dev.cash === 220); // += 100 * 1.2
dev.greet("Nick"); // Hey Nick. I'm Jane

The _super field value changes from method call to method call, so it may be used only in the method that has overridden the base method. Do not use it in a nested function unless you know what you are doing.

Traits (mixins)

Traits allow you to keep common functionality of different class hierarchies in a separate object called trait. When a class mixes a trait in, all trait's members are imported into the class. To mix a trait put it into an array together with a base class (if any). You can put as many traits in _super as you want.

var MusicFan = {
  listenMusic: function () {
    console.log("$ mpg321 -B ~/music");
  }
};

var Developer = defineClass({
  _super: [Person, MusicFan]
  // ...
});

var jane = new Developer("Jane", "Doe", "Python");
jane.listenMusic();

In fact, the above definition is equivalent to:

var tempClass = defineClass({
  _super: Person,
  listenMusic: function () {
    console.log("Listening music");
  }
});

var Developer = defineClass({
  _super: tempClass
  // ...
});

This transformation is called type linearization and was borrowed from Scala programming language. In fact trait mixing works the same way as class inheritance.

Calling class methods in traits

Since trait methods are called in the context of a class instance, a trait can call class methods although they are not a part of the trait definition.

// define a trait
var GreetEverybody = {
  greetEverybody: function () {
    // this trait assumes that the classes that mix it in, contain a "greet" method
    this.greet("everybody");
  }
};

// inherit from a class and mix a trait in
var Boss = defineClass({
  _super: [Person, GreetEverybody], // inherit from Person and mix GreetEverybody in
  // ...
});

var b = new Boss("Robert", "Roe");
// call a trait method
b.greetEverybody(); // Hello, everybody. My name is Robert Roe.

Overriding base class methods with traits

A trait method can override a base method and call it as this._super:

var HowAreYou = {
  greet: function (name) {
    this._super(name);
    console.log("How are you?")
  }
};

var NiceWheather = {
  greet: function (name) {
    this._super(name);
    console.log("The wheather is nice today, isn't it?")
  }
};

var Chatty = defineClass({
  _super: [Person, HowAreYou, NiceWheather]
});

var c = new Chatty("John", "Doe");
c.greet("Bob");
// Output:
//   Hello, Bob. My name is John Doe.
//   How are you?
//   The wheather is nice today, isn't it?

Trait order in _super

The order of traits in the _super array is important because it controls the trait application order:

var Chatty2 = defineClass({
  _super: [Person, NiceWheather, HowAreYou]
});

var c2 = new Chatty("John", "Doe");
c2.greet("Bob");
// Output:
//   Hello, Bob. My name is John Doe.
//   The wheather is nice today, isn't it?            <--- the order
//   How are you?                                     <--- has changed

We've changed the trait order and the output has changed.

Proxy classes

defineClass.proxy(class, fieldName="_real")
defineClass.proxy(methodNameArray, fieldName="_real")

Parameters class and methodNameArray specify method names to define in the proxy class.

Parameter fieldName specifies a name of a field where the instance with a real implementation is stored.


var PersonProxy = defineClass.proxy(Person);
var anna = new Person("Anna", "Litau");
var proxy = new PersonProxy(anna);
proxy.greet("Nodir"); // Hi Nodir. My name is Anna.

Method delegation using proxy traits

Beside inheriting from proxy classes, combined with traits together they allow you to easily define a class that delegates its functionality to some other objects stored in its fields.

var Foo = defineClass({
  doFoo: function () { /*...*/ }
});

var Bar = defineClass({
  doBar: function () { /*...*/ }
});

var Qux = defineClass({
  _super: [
    defineClass.proxy(Foo, "_foo"),
    defineClass.proxy(Bar, "_boo")
  ],

  constructor: function () {
    this._foo = new Foo();
    this._bar = new Bar();
  }
});

var qux = new Qux();
qux.doFoo();
qux.doBar();

Nested classes

Decreasing complexity

When a method of a class gets too complex, it is convenient to refactor it and implement its functionality as a nested class.

var Foo = defineClass({
    // define a nested class
    Bar: defineClass({
        qux: 1,
        compute: function () {
          return this.qux * 2;
        }
    }),

    // create a Bar
    compute: function () {
        var bar = new this.Bar(); // we write "new this.Bar" because it is located in the Foo.prototype
        return bar.compute();
    }
});

var foo = new Foo();
assert(foo.compute() === 2);

Nested class overriding

Since Bar is a member of Foo, a class derived from Foo can override it:

// derive from Foo
var Foo2 = defineClass({
    _super: Foo,

    // override a nested class
    Bar: defineClass({
        _super: Foo.prototype.Bar,
        qux: 2
    })
});

var foo2 = new Foo2();
assert(foo2.compute() === 4);

Note that

  1. The bar method creates a Bar class instance as new this.Bar(). This is flexible because this.Bar can be overridden in a subclass.
  2. Since the Bar class is overridden in the Foo2, it is unnecessary to override the bar method.

Special notation

There is a special notation for class overriding:

var Foo2 = defineClass({
    _super: Foo,

    // just an object
    Bar: {
        // no need to specify _super

        qux: 2
    }
});

var foo = new Foo2();
assert(foo.compute() === 4);

defineClass realizes that there is a nested class of the same name in the base class, so it just redefines the new Bar and inherits from the base Bar.

npm loves you