Rob Kinyon - Class-0.05

Documentation | Source

NAME

Class

DESCRIPTION

This provides a full single-inheritance class system for JavaScript via Class.subclass. See below for details.

SYNOPSIS

    JSAN.use('Class');

    MyClass = Class.subclass("MyClass", Object, {
        initialize: function (myVariable) {
            this.myVaraible = myVariable;
        }
    };

    MySubclass = Class.subclass("MySubclass", MyClass, {
        initialize: function (myVariable, myOtherVariable) {
            //
            // Alternative to:
            //    MyClass.prototype.initialize.apply(this, [myVariable])
            //
            arguments.callee.nextMethod(this, myVariable);
            this.myOtherVariable = myOtherVariable;
        }
    };

DEPENDENCIES

Class requires an ECMA-262 revision 3 (1999) compliant ECMAScript implementation.

Specially, any "modern" JavaScript interpreter with exceptions, anonymous function syntax, Function.prototype.push and Function.prototype.apply. This has been rigorously tested on Safari 2.0, Firefox 1.0, and Internet Explorer 6 (in that order).

OBJECT ORIENTED JAVASCRIPT PRIMER

Prototypes

JavaScript is a non-traditional Object Oriented language in that it uses prototype based, rather than class based inheritance. Some other languages that implement prototype-based OO are Self, Lua, Io, NewtonScript, Slate, and Prothon.

The basic premise of prototype-based OO is that new objects are created from similar objects, much like using a copy machine. The new object is largely indistinguishable from the prototype used to create it. It is possible to modify the new object without affecting the prototype, and this new object may be used as the prototype for other objects.

Functions

In JavaScript, functions are first-class objects like any other. They may be stored in variables, passed as arguments, and properties may be added or removed from them dynamically (e.g. func.foo = "bar"). Functions are also the building block for creating new objects. There are three different syntaxes for invoking a function:

Function Call: func(a, b)

Execute the body of func with two arguments, a and b, and the special this variable set to the global object (typically window in a browser). The value of this expression will be the value given by the terminal return statement, or undefined if no return statement is reached before the execution completes.

Note that even if func is a local variable in another function's scope, this will still be set to the global object!

Method Call: obj.func(a, b)

Execute the body of func with two arguments, a and b, and special this variable set to obj. The value of this expression will be the value given by the terminal return statement, or undefined if no return statement is reached before the execution completes.

Note that this is actually a different syntax entirely than the function call. func is not bound to obj in any way, and that a reference to func as a local variable or on another object will behave differently if called!

Constructor: new func(a, b)

Execute the body of func with two arguments, a and b, and special this variable set to a new object (newObject) cloned from func.prototype. The value of this expression is always newObject. If any return statement is reached during execution, its value is ignored.

The expression (newObject instanceof func) will evaluate to true so long as func.prototype is not replaced with another object. Additionally, (newObject instanceof T) will evaluate to true for all values of T where (func.prototype instanceof T) evalutes to true.

newObject is a copy-on-write clone of func.prototype. This means that any property set on newObject will not propagate to func.prototype. Conversely, any property added or changed on func.prototype will propagate to newObject, unless that property has been set on newObject.

All objects, including func.prototype, have this prototype property delegation behavior. This means that properties from func.prototype's prototype will propagate to newObject unless otherwise set by func.prototype or newObject, and so on. This prototype property delegation chain terminates with Object.prototype, so properties set on Object.prototype are accessible (and enumerable) from every object in the whole interpreter! Due to this, it's generally wise to avoid modifying Object.prototype (or really, any built-in prototype) whenever possible.

Demonstration

    // set up the prototype
    func.prototype.protoProperty = 1;
    func.prototype.anotherProperty = 2;

    // create a new object from the prototype
    var newObject = new func(a, b);

    // the prototype's properties are visible on newObject
    assert( newObject.protoProperty == 1 );
    assert( newObject.anotherProperty == 2 );

    // newObject can be modified without affecting its prototype
    newObject.anotherProperty = 3;
    assert( newObject.anotherProperty == 3 );
    assert( func.prototype.anotherProperty == 2 );

    // func.prototype can be modified, and it will affect
    // newObject unless the modified properties were written to
    // in newObject
    func.prototype.protoProperty = 0;
    func.prototype.anotherProperty = 1;
    assert( newObject.protoProperty == 0 );
    assert( newObject.anotherProperty == 3 );

NAMESPACES

Class

The Class namespace is a placeholder for functions used to implement a single-inheritance class system for JavaScript. Currently, the only function available is subclass.

  • subclass(name[, superclass[, body]])
  • Returns newClass, a function that will create new objects (either just by calling it, or using the new operator).

    The parameters are as follows:

    • No parameters
    • A class will be created with no functions that is a child of Object.

    • One parameter
    • The parameter is expected to be an object containing the methods and attributes you wish to set in the prototype. The superclass will be Object.

    • Two parameters
    • The first parameter will either be the name or the superclass object. If a name, then the superclass is Object. The second parameter will be the object containing the methods and attributes you wish to set in the prototype.

    • Three parameters
    • The first will be the name, the second the superclass and the third the object containing the methods and attributes you wish to set in the prototype. If the superclass evaluates to false, it will be set to Object.

    The following properties of newClass will be set up by subclass:

    • NAME Set to the given name, and will be used in the default repr and toString implementation of newClass.
    • superClass
    • Set to the given superClass, or Object.

    • prototype
    • Will be constructed by calling new newClass.superClass(). If superClass was originally created by Class.subclass, then it will be called with a special form that does not invoke the initialize function.

      If body is given, then every enumerable property from body will be set on prototype. Functions are treated in a special manner to support nextMethod(). If the function was created by Class.subclass, or has a __class__ property, then it is left as-is. Otherwise, the function is modified, and the following properties are set:

      • NAME
      • The fully-qualified name of the function: [newClass.NAME, propertyName].join(".")

      • __class__
      • A reference to newClass

      • __name__
      • The property name of the function in body

      • getNextMethod()
      • Locate the implementation of this method (by name) in superClass.prototype. If no implementation exists, a TypeError will be thrown.

        Use this if the superClass implementation takes the same arguments:

            arguments.callee.getNextMethod().apply(this, arguments);
      • nextMethod(this, arg1, arg2, ...)
      • Locate the implementation of this method (by name) in superClass.prototype, and call it with the given object and arguments.

        Use this if the superClass implementation takes different (or no) arguments, just like you would use Function.prototype.call:

            arguments.callee.nextMethod(this, arg1, arg2);

      subclass() calls create() and extend(). create() takes the same parameter list and will call extend if a body is defined. extend() is used to create a subclass. It takes three parameters ( thisClass, superClass, extension ) and sets thisClass.prototype to the prototype based on superClass and extension.

      It is strongly recommended that you call subclass() in all cases.

    • CAVEATS
    • When an identical function is present in body as more than one property, or was used in another class body, the behavior here is undefined! Don't do that if you expect to use nextMethod or getNextMethod.

      Only functions present in body will have these features. In other words, functions added to newClass.prototype after creation are not modified.

      For these two situations, the superClass implementation must be called explicitly as follows:

          superClass.prototype.methodName.apply(this, arguments);

    Instances of newClass may be created with function call syntax (newClass()) or the new operator (new newClass()). If an initialize property exists on the prototype, it will be called on the new instance with all of the arguments passed to the constructor.

    Instances of newClass obey idiomatic JavaScript rules, most notably:

        newClass = Class.subclass("newClass", superClass);
        var inst = new newClass();
        assert( inst instanceof newClass && inst instanceof superClass );

    Instances of newClass will additionally have the following properties upon construction:

    __class__

    A reference to newClass

    __id__

    A unique (per interpreter) integer identifier for this instance. The default toString and __repr__ use this. It's particularly useful because you can use it as a "hash" for the instance, and it's instrumental when debugging.

SUPPORT

Currently, there is no mailing list or IRC channel. Please send bug reports and patches to the maintainer.

COPYRIGHT

This software is released into the public domain. No-one holds the copyright.

AUTHOR

Written by Bob Ippolito

Interface initially designed by Sam Stephenson for the Prototype library

Maintained by Rob Kinyon (rob.kinyon@iinteractive.com)

My time is generously donated by Infinity Interactive, Inc. http://www.iinteractive.com

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 178:

=back doesn't take any parameters, but you said =back 4

Around line 245:

Expected '=item *'

/*

*/

if (typeof(Class) == 'undefined') {
    Class = {};
}

Class.NAME = 'Class';
Class.VERSION = '0.04';
Class.__repr__ = function () {
    return "[" + this.NAME + " " + this.VERSION + "]";
}
Class.toString = function () {
    return this.__repr__();
}
Class.EXPORT = [];
Class.EXPORT_OK = ['create', 'extend', 'subclass'];
Class.EXPORT_TAGS = {
    ':all': Class.EXPORT_OK,
    ':common': Class.EXPORT
};

/*

*/

Class.__new__ = function () {
    // private token, naked unique object
    var __clone__ = {};
    // the incrementing counter for instances created
    var instanceCounter = 0;
    // This is the representation of class objects
    var classToString = function () {
        return "[Class " + this.NAME + "]";
    };
    // Representation of instance objects
    var instanceToString = function () {
        return "[" + this.__class__.NAME + " #" + this.__id__ + "]";
    };
    var forwardToRepr = function () {
        return this.__repr__();
    };
    var proxyFunction = function (func) {
        var callFunc = func.__orig__;
        if (typeof(callFunc) == 'undefined') {
            callFunc = func;
        }
        var newFunc = function () {
            return callFunc.apply(this, arguments);
        }
        for (var k in func) {
            newFunc[k] = func[k];
        }
        newFunc.__orig__ = callFunc;
        return newFunc;
    };

    var getNextMethod = function (self) {
        var next_method = null;
        try {
            return this.__class__.superClass.prototype[this.__name__];
        } catch (e) {
            throw new TypeError("no super method for " + this.NAME);
        }
    }
        
    var nextMethod = function (self) {
        var args = [];
        for (var i = 1; i < arguments.length; i++) {
            args.push(arguments[i]);
        }
        var next = this.getNextMethod();
        if ( typeof( next ) == 'function' ) {
            next.apply(self, args);
        }
    };

    this.create = function () {
        var body = null;
        var name = "Some Class";
        var superClass = Object;

        if ( arguments.length == 1 ) {
            body = arguments[0];
        }
        else if ( arguments.length == 2 ) {
            if ( typeof arguments[0] == 'string' ) {
                name = arguments[0];
            }
            else {
                superClass = arguments[0];
            }
            body = arguments[1];
        }
        else {
            name = arguments[0];
            superClass = arguments[1];
            body = arguments[2];
        }

        // this is the constructor we're going to return
        var rval = function (arg) {
            // allow for "just call" syntax to create objects
            var o = this;
            if (!(o instanceof rval)) {
                o = new rval(__clone__);
            } else {
                o.__id__ = ++instanceCounter;
            }
            // don't initialize when using the stub method!
            if (arg != __clone__) {
                if (typeof(o.initialize) == 'function') {
                    o.initialize.apply(o, arguments);
                }
            }
            return o;
        };

        rval.NAME = name;
        rval.superClass = superClass;
        rval.toString = forwardToRepr;
        rval.__repr__ = classToString;
        rval.__MochiKit_Class__ = true;

        if ( body ) {
            this.extend( rval, superClass, body );
        }

        return rval;
    };

    this.extend = function ( rval, superClass, body ) {

        var proto = null;
        if (superClass.__MochiKit_Class__) {
            proto = new superClass(__clone__);
        } else {
            proto = new superClass();
        }

        if (typeof(proto.toString) == 'undefined' || (proto.toString == Object.prototype.toString)) {
            proto.toString = instanceToString;
        }
        if (typeof(proto.__repr__) == 'undefined') {
            proto.__repr__ = instanceToString;
        }
        if (proto.toString == Object.prototype.toString) {
            proto.toString = forwardToRepr;
        }
        if (typeof(body) != 'undefined' && body != null) {
            for (var k in body) {
                var o = body[k];
                if (typeof(o) == 'function' && typeof(o.__MochiKit_Class__) == 'undefined') {
                    if (typeof(o.__class__) != 'undefined') {
                        if (o.__class__ != rval) {
                            continue;
                        }
                        o = proxyFunction(o);
                    }
                    o.__class__ = rval;
                    o.__name__ = k;
                    o.NAME = rval.NAME + '.' + k;
                    o.nextMethod = nextMethod;
                    o.getNextMethod = getNextMethod;
                }
                proto[k] = o;
            }
        }
        proto.__id__ = ++instanceCounter;
        proto.__class__ = rval;

        proto.__super__ = function ( methname ) {
            if ( typeof( this[methname] ) != 'function' ) return; 
            var args = [];
            for ( var i = 1; i < arguments.length; i++ )
                args.push( arguments[i] );

            this[methname].nextMethod( this, args );
        };

        proto.__super__.__class__ = superClass;
        proto.__super__.__name__ = '__super__';
        proto.__super__.NAME = rval.NAME + '.__super__';
        proto.__super__.nextMethod = nextMethod;
        proto.__super__.getNextMethod = getNextMethod;

        rval.prototype = proto;
    };

    this.subclass = function () {
        var body = {};
        var name = "Some Class";
        var superClass = Object;

        if ( arguments.length == 1 ) {
            body = arguments[0];
        }
        else if ( arguments.length == 2 ) {
            superClass = arguments[0];
            body = arguments[1];
        }
        else {
            name = arguments[0];
            superClass = arguments[1];
            body = arguments[2];
        }

        var rval = this.create( name, superClass, body );
        this.extend( rval, superClass, body );

        return rval;
    };
    this.subclass.NAME = this.NAME + "." + "subclass";
};

Class.__new__();
/*

*/

/*

*/