Classical Object-Oriented
JavaScript Framework

Download 0.2.9
Released: 07 Nov 2017

GNU ease.js is a Classical Object-Oriented framework for JavaScript, intended to eliminate boilerplate code and “ease” the transition into JavaScript from other Object-Oriented languages. Features include:

GNU ease.js is a framework, not a compiler. It may be used wherever JavaScript may be used, and supports all major browsers; ease.js also provides support for older, pre-ES5 environments by gracefully degrading features (such as visibility support) while remaining functionally consistent.

This project is part of the GNU Project.

Simple and Intuitive Class Definitions

Class definitions closely resemble the familiar syntax of languages like Java and PHP.

const { Class } = easejs;

const Stack = Class( 'Stack',
{
    'private _stack': [],

    'public push'( value )
    {
        this._stack.push( value );
    },

    'public pop'()
    {
        return this._stack.pop();
    },
} );

Classes can be anonymous or named, the latter being more useful for debugging. Since classes may be anonymous, constructors are styled after PHP.

const Foo = Class(
    'private _name': '',

    constructor( name )
    {
        this._name = ''+( name );
    },

    'public sayHello'()
    {
        return this._name + " says 'Hello!'";
    },
);

Classes can be instantiated with or without the new keyword. Omission aids in concise method chaining and the use of temporary instances.

const inst_a = Foo( "John Doe" );
const inst_b = new Foo( "John Doe" );

// temporary instance
Foo( "John Doe" ).sayHello();
→ Read more in manual

Classical Inheritance

Classes can be extended to create subtypes. Like C++, methods are not virtual by default. In Java terminology, all methods are final by default. Multiple inheritance, like Java, is unsupported (see Interfaces).

const Cow = Class( 'Cow',
{
    'virtual public tip'()
    {
        return "Omph.";
    },
} );

const SturdyCow = Class( 'SturdyCow' )
    .extend( Cow,
{
    'override public tip'()
    {
        return "Moo.";
    },
} );

Alternatively, if creating an anonymous subtype, the supertype's extend() method may be used.

const SturdyCow = Cow.extend( { /*...*/ } );

Type checks for polymorphic methods may be performed with Class.isA(), which is recommended in place of instanceof.

const cow    = Cow();
const sturdy = SturdyCow();

Class.isA( Cow, cow );           // true
Class.isA( SturdyCow, cow );     // false
Class.isA( Cow, sturdy );        // true
Class.isA( SturdyCow, sturdy );  // true

To prevent a class from being extended, FinalClass may be used.

const Foo = FinalClass( 'Foo',
{
    'public describe'()
    {
        return "I cannot be extended.";
    },
} );
→ Read more in manual

Traits As Mixins

Trait support was introduced in celebration of becoming a GNU project. It is currently under development and has not yet been finalized, but has been included in each GNU ease.js release since v0.2.0, and is stable.

const Echo = Class( 'Echo',
{
    'virtual public echo'( str )
    {
        return str;
    },
} );

const Prefix = Trait( 'Prefix' )
    .extend( Echo,
{
    'private _prefix': '',

    __mixin( prefix )
    {
        this._prefix = ''+prefix;
    },

    'public abstract override echo'( str )
    {
        return this._prefix + this.__super( str );
    },
} );

const Suffix = Trait( 'Suffix' )
    .extend( Echo,
{
    'private _suffix': '',

    __mixin( suffix )
    {
        this._suffix = ''+suffix;
    },

    'public abstract override echo'( str )
    {
        return this.__super( str ) + this._suffix;
    },
} );

const UpperCase = Trait( 'UpperCase' )
    .extend( Echo,
{
    'public abstract override echo'( str )
    {
        return this.__super( str ).toUpperCase();
    }
} );

// stackable, parameterized traits
Echo.use( Prefix( "Bar" ) )
    .use( Suffix( "Baz" ) )
    .use( UpperCase )
    .use( Prefix( "Foo" ) )
    .use( Suffix( "Quux" ) )().echo( "Inner" );

// result: FooBARINNERBAZQuux

Documentation will be available once some final details are finalized. Until that time, the test cases provide extensive examples and rationale. The following posts also summarize some of the features:

Access Modifiers

All three common access modifiers—public, protected and private—are supported, but enforced only in ECMAScript 5 and later environments.

const DatabaseRecord = Class( 'DatabaseRecord',
{
    'private _connection': null,


    constructor( host, user, pass )
    {
        this._connection = this._connect( host, user, pass );
    },

    'private _connect'( host, user, pass )
    {
        // (do connection stuff)
        return { host: host };
    },

    'protected query'( query )
    {
        // perform query on this._connection, rather than exposing
        // this._connection to subtypes
    },

    'protected escapeString'( field )
    {
        return field.replace( "'", "\\'" );
    },

    'public getName'( id )
    {
        return this._query(
            "SELECT name FROM users WHERE id = '" +
            this._escapeString( id ) + "' LIMIT 1"
        );
    },
} );

In the above example, the database connection remains encapsulated within DatabaseRecord. Subtypes are able to query and escape strings and external callers are able to retrieve a name for a given id. Attempting to access a private or protected member externally will result in an error. Attempting to access a private member from within a subtype will result in an error.

Alternatively, a more concise style may be used, which is more natural to users of JavaScript's native prototype model:

const DatabaseRecord = Class( 'DatabaseRecord',
{
    /* implicitly private */
    _connection: null,


    constructor( host, user, pass )
    {
        this._connection = this._connect( host, user, pass );
    },

    /* implicitly private */
    _connect( host, user, pass )
    {
        // (do connection stuff)
        return { host: host };
    },

    'protected query'( query )
    {
        // perform query on this._connection, rather than exposing
        // this._connection to subtypes
    },

    'protected escapeString'( field )
    {
        return field.replace( "'", "\\'" );
    },

    /* public by default */
    getName( id )
    {
        return this._query(
            "SELECT name FROM users WHERE id = '" +
            this._escapeString( id ) + "' LIMIT 1"
        );
    },
} );
→ Read more in manual

Abstract Classes and Methods

If a class contains abstract members, it must be declared as an AbstractClass. Abstract methods must be overridden by subtypes and are implicitly virtual.

const Database = AbstractClass( 'Database',
{
    'public connect'( user, pass )
    {
        if ( !( this.authenticate( user, pass ) ) )
        {
            throw Error( "Authentication failed." );
        }
    },

    // abstract methods define arguments as an array of strings
    'abstract protected authenticate': [ 'user', 'pass' ],
} );

const MongoDatabase = Class( 'MongoDatabase' )
    .extend( Database,
{
    // must implement each argument for Database.authenticate()
    'protected authenticate'( user, pass )
    {
        // ...
    },
} );
→ Read more in manual

Interfaces

ease.js supports the Java concept of Interfaces, which act much like abstract classes with no implementation. Each method is implicitly abstract. Properties cannot be defined on interfaces.

const Filesystem = Interface( 'Filesystem',
{
    'public open': [ 'path', 'mode' ],

    'public read': [ 'handle', 'length' ],

    'public write': [ 'handle', 'data' ],

    'public close': [ 'handle' ],
} );

Concrete classes may implement one or more interfaces. If a concrete class does not provide a concrete implementation for every method defined on the interface, it must be declared an AbstractClass.

const ConcreteFilesystem = Class( 'ConcreteFilesystem' )
    .implement( Filesystem )  // multiple interfaces as separate arguments
{
    'public open'( path, mode )
    {
        return { path: path, mode: mode };
    },

    'public read'( handle, length )
    {
        return "";
    },

    'public write'( handle, data )
    {
        // ...
        return data.length;
    },

    'public close'( handle )
    {
        // ...
        return this;
    },
} );

Polymorphic methods may check whether a given object implements a certain interface.

const inst = ConcreteFilesystem();
Class.isA( Filesystem, inst ); // true
→ Read more in manual

Static and Constant Members

Static members are bound to the class itself, rather than a particular instance. Constants are immutable static members (unlike languages like PHP, they may use any access modifier). In order to support both pre- and post-ECMAScript 5 environments, the syntax requires use of a static accessor method—$().

const Cow = Class( 'Cow',
{
    'const LEGS': 4,

    'private static _number': 0,

    constructor()
    {
        // __self refers to the class associated with this instance
        this.__self.$( '_number' ) = this.__self.$( '_number' ) + 1;
    },

    'public static create'()
    {
        return Cow();
    },

    'public static getNumber'()
    {
        return this.__self.$( '_number' );
    },
} );

Cow.$( 'LEGS' ); // 4
Cow.getNumber(); // 0
Cow.create();
Cow.getNumber(); // 1
→ Read more in manual

Transparent Error Subtyping

Error subtyping (creating your own error types) in ECMAScript is notoriously crude, and getting it to work intuitively is even harder. ease.js transparently handles all necessarily boilerplate when extending Error or its subtypes.

const MyError = Class( 'MyError' )
    .extend( Error, {} );

const e = MyError( 'Foo' );
e.message;  // Foo
e.name;     // MyError

// -- if supported by environment --
e.stack;         // stack beginning at caller
e.fileName;      // caller filename
e.lineNumber;    // caller line number
e.columnNumber;  // caller column number

// general case
throw MyError( 'Foo' );
→ Read more in manual