Enumeration and Object Oriented JavaScript

 

*AnimTree
*Tabs
*GlideMenus
*DragLib

Borrowing Using Library Code

Borrow Properties Ignored by IE and Other Browsers

The solution is to make the fucking browsers support the language!

OK, ok, IE is crap, JavaScript sucks, blah, blah... time to stop complaing and roll my sleeves up.

I've just addressed borrowing here. I'll show how to create an array from an object next.

The borrow function is unrefined. In developing your own library, you should work on packaging and API design.

The approach I've taken for borrowing is to create an Array of names from properties of Object.prototype that have the DontEnum attribute, objectDontEnums. (§15.2.4.7).

  1. Enumerate over the supplier using for in, copying properties using a given shadow/replace strategy (see the code for details).
  2. iterate over objectDontEnums.
  3. in each iteration, set P as the value of objectDontEnums[ i ].
  4. If the supplier has a property with name P, and that property should, according to the specification, have the DontEnum attribute, copy the property in the receiver, based on the given shadow/replace strategy.

The Full List of DontEnum properties in Object.prototype

toString
toLocaleString
valueOf 
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
constructor
Function instance and Function.prototype properties that have DontEnum.
prototype
call
apply

There are many more properties of other objects that have the DontEnum attribute. For example, Number.MAX_VALUE has the attributes { DontEnum, DontDelete, ReadOnly }. If we create a Number object and add a MAX_VALUE property to that number object, IE will skip that property.

We can avoid dealing with these imaginary problems by limiting our function to support only Function and Object instances (YAGNI).

Use Object.prototype.hasOwnProperty

We can use hasOwnProperty safely.

(function(){

var obj = {
   toString : function() {
      return "obj to string";
   }
};
return obj.hasOwnProperty("toString");

})();

Result:

Should be: true

Browser: Internet Explorer Mozilla Opera Safari 2 Safari 3
Result: true true true true true

YUI does not use hasOwnProperty and I think it is because they are trying to support Safari 1.3.

The idea is the same thing as augmentObject; copy properties from one object to another. The difference is it considers shadowing separately from replacing.

The method signature will take four parameters.

borrow( receiver, supplier, shadow, override )
/**
 * 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 ];
    }
}

Analysis

Bloated, Complex?

Unfortunately, one of the pitfalls with having to support Internet Explorer is bloated, inefficient, and unnecessarily complex code.

Fortunately, MSIE is working with TG1 and will be implementing ECMAScript 4. This will resolve problems with enumeration. In ES4, smaller, faster, and less buggy library code will be possible.

TypeError?

borrow throws a TypeError if you pass it something that's not an Object or a Function. This is used here as an equivalent of Java's IllegalArgumentException. It helps us discover a problem as soon as possible.

Type checking is something that JavaScript does not make easy. Built-ins like String or Date objects can slip through our function.

borrow({}, "oops"); // borrow would throw a TypeError
borrow(null, function(){}) // Error 

Function.extend can use Object.borrow

We can now use borrow with extend. The default behavior used here is to shadow properties in the receiver, but not replace them.

(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);
    }
};

})();

Borrow or Extend?

Use extend when you want to chain constructors and methods. Having a clearly defined interface can make your code more explicit and obvious.

Use borrow when you want to add additional behavior to an object. Having a shallow inheritance hierarchy is almost always easier to deal with. Borrowing adds behavior. It can be thought of as a substitute for a Decorator pattern.

Ideally, your objects will be so simple that you won't need either borrow or extend.

If you're writing library code, it's necessary to study OOA&D. Some useful readings I've found: Head First Design Patterns, Refactoring, and Domain Driven Design.

Testing Object.borrow and Function.extend

I've created a deep prototype chain in two examples.

  1. DatedProgrammer
  2. DatedCard

The examples are useful to the point of testing the code.

JavaScript Needs a DateFormatter

Dates cannot be formatted to localized formats in JavaScript. Method Date.prototype.toLocaleString returns an implementation-dependent string which has varied results. The format cannot be relied upon. Date Strings cannot be reliably parsed by a programmer. The built-in Date.parse is not guaranteed to parse localized date strings.

Java's SimpleDateFormat solves these problems.

Next in this tutorial: