Prototype Inheritance and Borrowing: DatedProgrammer Example

Create a two concrete classes, one with an "abstract" superclass (just pretend BaseConstructor is abstract).

DatedProgrammer has a superclass, BaseConstructor and shadows its toString method.

BaseConstructor
    | 
    +DatedProgrammer

Sortable

Borrow methods from Sortable to DatedProgrammer but don't shadow or replace any methods.

Object.borrow(DatedProgrammer.prototype, Sortable.prototype);
(function(){
/**
 * Borrow properties from supplier to receiver using a for-in loop.
 *
 * Does not copy values from the supplier's prototype; uses hasOwnProperty. 
 * this prevents things like "constructor" and "prototype" from slpping through 
 * inconsistantly in some browsers. However, if the object is a Function, and contains
 * a "prototype" property that is enumerable, this property will be copied over; forcefully, 
 * if the browser incorrectly skips it in enumeration.
 *
 * The only property that will be skipped is "constructor". In JScript, there is no way 
 * to know if a constructor property is enumerable or not. In most cases, it is not. If you have 
 * added a "constructor" property to the supplier, and want that copied to the receiver, 
 * it must be done manually, e.g. r.constructor = s.constructor.
 * 
 * @param {Function|Object} receiver  The object that is to receive the properties. 
 * Cannot be  null or undefined.
 *
 *
 * @param {Function|Object} supplier  The object that is to supply the properties. 
 * Cannot be a built-in, but may be a subclass of any other object. May be a function.
 *
 * @param {Boolean} shadow  if true, properties that exist in 
 * the receiver's prototype, but not the receiver, will be copied to the receiver.
 *
 * @param {Boolean} replace  if true, every property that exists in the supplier 
 * will be copied to the receiver.
 *
 * @throws {TypeError}  Thrown if receiver or supplier is neither typeof 
 * "object" or typeof "function"
 * 
 * This provides for four possible use-cases
 * 1. receiver has a property in its prototype and wants to borrow properties from supplier,
 * but does not want to affect it's own behavior:
 * borrow( receiver, supplier ); // 
 *
 * 2. receiver has a property in its prototype, but wants to get every method from 
 * supplier, even if it shadows something it receiver's superclass.
 * borrow( receiver, supplier, true ); // most common.
 *
 * 3. receiver wants to borrow everything from supplier, even when 
 * certain properties would replace values in the receiver's own instance.
 * borrow( receiver, supplier, true, true ); // common
 * 
 * 4. receiver wants to borrow everything from supplier, even when certain properties would 
 * replace values in the receiver's own instance, but does not want to shadow any properties 
 * that occur in the receiver's prototype. 
 * borrow( receiver, supplier, false, true ); // uncommon.
 * `
 */

Object.borrow = function borrow( receiver, supplier, shadow, replace ) {

// Throw an error if supplier or receiver is not an Object or a Function.
    if(typeof supplier != "object" && typeof supplier != "function") 
        throw new TypeError("borrow failed: supplier not an object or function: " + typeof supplier);
    if(typeof receiver != "object" && typeof receiver != "function") 
        throw new TypeError("borrow failed: receiver not an object or function: " + typeof supplier);
    for( var prop in supplier) {
        var rp = receiver[ prop ];
        if( rp === supplier[ prop ] || !supplier.hasOwnProperty( prop ) ) continue;

        // User must explicitly copy this over.
        if( prop == "constructor" ) continue;

        if ( shadow && replace ) { // shadow and replace 
            receiver[ prop ] = supplier[ prop];
        }
        else if( shadow ) { // shadow, don't replace
            if( !receiver.hasOwnProperty( prop ) )
                receiver[ prop ] = supplier[ prop ];
        }
        else if( replace ) { // replace, don't shadow
            if( receiver.hasOwnProperty( prop ) &&!(prop in receiver.constructor.prototype))
                receiver[ prop ] = supplier[ prop ];
        }
        else if( !(prop in receiver) ) { // don't shadow, don't replace
            // don't shadow, don't replace.
            receiver[ prop ] = supplier[ prop ];
        }
    }
    if(shadow)
        dontEnumFix(receiver, supplier, replace );     
};

/** 
 * All shadowed properties
 */
function dontEnumFix(r,s, replace ) {

    // JScript goes wrong here.
    var objectDontEnums = [
// For "constructor" the user must explicitly copy it over.
//      "constructor"
        "toString"
        ,"toLocaleString"
        ,"isPrototypeOf"
        ,"propertyIsEnumerable"
        ,"hasOwnProperty"
        ,"valueOf"
    ];

    if( typeof s == "function" && typeof s.call == "function" ) {
        objectDontEnums.push("call", "apply", "prototype");
    }
    
    for( var i = 0; i < objectDontEnums.length; i++ ) {
        var p = objectDontEnums[ i ];
        if(!s.hasOwnProperty( p ) || r[ p ] === s[ p ] ) continue;
        if ( replace || !r.hasOwnProperty(p) ) 
            r[ p ] = s[ p ];
    }
}
})();
(function(){
/**
 * @param {prototypeShadows} - extra properties to add to the subclass' prototype
 */ 
Function.extend = function extend(subc, superc, prototypeShadows) {
    if (typeof subc != "function" || typeof superc.call != "function") {
         throw new TypeError("extend: subclass is not a function " + subc);
    }
    if (typeof superc != "function" || typeof superc.call != "function") {
         throw new TypeError("extend: superclass is not a function: " + superc);
    }
    // Fix for JavaScript's broken constructor.

    var F = Function();
    F.prototype = superc.prototype;
    subc.prototype = new F();
    subc.prototype.constructor=subc; 
    subc.superclass = superc;
    if (superc.prototype.constructor === Object) {
        superc.prototype.constructor=superc;
    }
    if (prototypeShadows) {    
        Object.borrow(subc.prototype, prototypeShadows, true);
    }
};

})();
(function(){
evalTextContent('borrow-source');
evalTextContent('function-extend-source');

//--------------------------------------------------------
// Implementation code:
// Here's where we set up the actual classes. 

/** 
 * @constructor BaseConstructor
 */
function BaseConstructor(){};

BaseConstructor.prototype = {
    toString : function /*getFunctionName*/() { 
        var match = /function\s+(\w+)/.exec(this.constructor);
        if(match)
            return match[1];
        return "";
    }
};

/** 
 * @constructor Sortable
 */
function Sortable() {} 
Sortable.prototype = {
    hashCode : function() {
        return Number(this.valueOf());
    }
    ,equals : function(other) {
        return this === other;
    }
    ,valueOf : function() {
        return new Date().getTime();
    }
};

/**
 * @constructor
 */
function DatedProgrammer(name, date) {
    // this.superclass.constructor.call(this);
    this._name = name;
    this._date = new Date(date);
};
// DatedProgrammer extends BaseConstructor

Function.extend(
    DatedProgrammer,
    BaseConstructor,
    {
    valueOf : function() {
        return this._date.valueOf();
    }
    ,toString : function() {
        // Chop off the "(Pacific Standard Time)" in FF and Safari.
        var dateString = this._date.toString().replace(/\(.*/, "");
        var superToString = 
            this.constructor.superclass.prototype.toString.call(this);
        return (superToString + ":"
        + String.nl
        + "  name: " +
        this._name + 
        ", date: " + dateString);
    }
    ,equals : function(o) {
        return o && (this.valueOf() == o.valueOf());
    }
});
Object.borrow(DatedProgrammer.prototype, Sortable.prototype);

var calDate = new Date("Tue, 11 Jan 2000 07:00:00 GMT-0800");
var japDate = new Date("Wed, 12 Jan 2000 00:00:00 GMT+900");

var garrett = new DatedProgrammer("Garrett", new Date(calDate));
var tomomi = new DatedProgrammer("Tomomi", new Date(japDate));

Template.evalTemplate("dont-enum-4-expected", ["calDate", calDate, "japDate", japDate]);
return garrett.toString() 
+ String.nl
+ tomomi.toString()
+ String.nl
+"garrett.equals(tomomi): " + garrett.equals(tomomi);
})();
Result Expected
       
DatedProgrammer:
  name: Garrett, date: ${calDate.toString().replace(/\(.*/, "")}
DatedProgrammer:
  name: Tomomi, date: ${japDate.toString().replace(/\(.*/, "")}
garrett.equals(tomomi): true
Browser: Result:
Internet Explorer DatedProgrammer: name: Garrett, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 DatedProgrammer: name: Tomomi, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 garrett.equals(tomomi): true
Mozilla DatedProgrammer: name: Garrett, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 DatedProgrammer: name: Tomomi, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 garrett.equals(tomomi): true
Opera DatedProgrammer: name: Garrett, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 DatedProgrammer: name: Tomomi, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 garrett.equals(tomomi): true
Safari 2 DatedProgrammer: name: Garrett, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 DatedProgrammer: name: Tomomi, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 garrett.equals(tomomi): true
Safari 3 DatedProgrammer: name: Garrett, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 DatedProgrammer: name: Tomomi, date: Tue, 11 Jan 2000 07:00:00 GMT-0800 garrett.equals(tomomi): true

Examining the Result

DatedProgrammer extended BaseConstructor shadowing the toString method there.

DatedProgrammer borrowed from Sortable with shadow=false, replace=false.

The variation in the formatting of dates is an annoying distraction. Also notice that I input a Japanese date and got back a date string in local time (PST).