Enumeration and Object Oriented JavaScript

 

*AnimTree
*Tabs
*GlideMenus
*DragLib

Borrowing Properties: Using Enumeration in Library Code

Borrowing properties is adding properties of a supplier object as properties to a receiver object. This is usually done using a for in loop.

API Design

Borrowing requires some planning in order to make the methods and properties generic. Array generics is a good example of well-designed generic functionality.

JavaScript library authors who provide functionality for borrowing properties from other objects. who have not experienced the JScript DontEnum bug will often be confused for a while. In fact, many experienced programmers have tripped over this bug.

The problem presents itself when defining library code that uses aggregation to borrow properties, that would shadow a same-named property in the prototype chain that has the DontEnum attribute.

Borrowing is a feature in almost every JavaScript framework. Unfortunately, the JScript DontEnum bug makes the code for borrowing unnecessarily complicated.

Bloated, Complex?

Unfortunately, one of the pitfalls with having to support Internet Explorer is having to write and test innefficeint, complex, and otherwise unnecessary code.

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

JavaScript Frameworks and Toolkits

To list some libraries: YUI, Prototype.extend() dojo.extend, jQuery.extend, Tibco's bless(), Dean Edwards' base all use this approach. Microsoft's Type.prototype.registerClass uses borrowing, but dodges the MSIE bullet by not allowing augmentation with methods such as toString.

This is not a library review article, but it seemed necessary to pick some real world, library code to analyze.

I picked YUI's code for analysis. The analysis might seem negative. It is critical and unbiased. Yahoo and Dojo were the most well-commented code I could find. Yahoo provides prototypical inheritance, which was an important part of this article. Dojo does not.

An explanation of dojo's approach would have been more lengthy and less relevant.

Prototype has different problems, but also contains false comments. Tibco was the most poorly authored and had no comments. Dean's Base has few comments. An explanation of Base would have required added comments and testing at many points. This probably would have been educationally rewarding, but might have been somewhat out of scope.

I'm not picking on Yahoo. All of the libraries have problems.

Real World Code for Borrowing: YAHOO.lang.augmentObject

Method augmentObject copies properties from s to r.

(function(){
YAHOO = { };

YAHOO.lang = {
    /**
     * Applies all properties in the supplier to the receiver if the
     * receiver does not have these properties yet.  Optionally, one or 
     * more methods/properties can be specified (as additional parameters). 
     * This option will overwrite the property if receiver 
     * has it already.  If true is passed as the third parameter, all 
     * properties will be applied and _will_ overwrite properties in 
     * the receiver.
     *
     * @method augmentObject
     * @static
     * @since 2.3.0
     * @param {Function} r  the object to receive the augmentation
     * @param {Function} s  the object that supplies the properties to augment
     * @param {String*|boolean}  arguments zero or more properties methods 
     *        to augment the receiver with.  If none specified, everything
     *        in the supplier will be used unless it would
     *        overwrite an existing property in the receiver. If true
     *        is specified as the third parameter, all properties will
     *        be applied and will overwrite an existing property in
     *        the receiver
     */
    augmentObject: function(r, s) {
        if (!s||!r) {
            throw new Error("Absorb failed, verify dependencies.");
        }
        var a=arguments, i, p, override=a[2];
        if (override && override !== true) { // only absorb the specified properties
            for ( i=2; i < a.length; i = i+1) {
                r[a[i]] = s[a[i]];
            }
        } else { // take everything, overwriting only if the third parameter is true
            for (p in s) { 
                if (override || !r[p]) { // Falsey values shadowed and replaced when overrides is false
                    r[p] = s[p];
                }
            }
            
            YAHOO.lang._IEEnumFix(r, s);// Forgot about overrides.

        }
    },
    
    _IEEnumFix: function(r, s) {
        if (YAHOO.env.ua.ie) {// Bad browser detection. 
            var add=["toString", "valueOf"];
            for (i=0;i < add.length;i=i+1) {
                var fname=add[i],f=s[fname];
                if (YAHOO.lang.isFunction(f) && f!=Object.prototype[fname]) {
                    r[fname]=f;
                }
            }
        }
    },

    isFunction : function( o ) {
        return typeof o === "function";
    }
};

// I'm not copying over YAHOO's full env and YAHOO_Config packages just for this example.
YAHOO.env = {
	ua : function() {
		var o = { ie:0, opera:0 }, ua=navigator.userAgent, m = ua.match(/Opera[\s\/]([^\s]*)/);
		if (m && m[1]) o.opera = parseFloat(m[1]); 
		else {
			m = ua.match(/MSIE\s([^;]*)/);
			if (m && m[1]) o.ie = parseFloat(m[1]);
		}
		return o;
	}()
};

//------------------------------------------------------------------

// Our receiver-to-be.
var r = { 
    _draggable : false
    ,isDraggable : function() { return this._draggable }
    ,valueOf : function() { return 1; }
};
// Our supplier-to-be.
var s = {
    _draggable : true
    ,valueOf : function() { return 2; }
    ,toString : function() { return 's'; }
};

// Try YUI.
YAHOO.lang.augmentObject(r, s);

return [
    r.isDraggable(),
    r.valueOf(),
    r.toString()
].join(String.nl); // true 1 [object Object].

})();
Result Expected Yahoo Expects
  true 1 [object Object] false 1 [object Object]
Browser: Internet Explorer Mozilla Opera* Safari 2 Safari 3
Result: true 2 s true 1 [object Object] true 1 [object Object] true 1 [object Object] true 1 [object Object]

*Identifying as Opera in userAgent header.

Analysis: YAHOO.lang.augmentObject

Bugs

  1. Falsey values shadowed and replaced when overrides is false
  2. Bad browser detection
  3. Doesn't consider shadowing
Falsey values shadowed and replaced when overrides is false

What happens if the receiver can resolve a property whose value evaluates to false in boolean context? In that case, the supplier's property value will replace/shadow the receiver's property value, even when overrides is not true. What if the receiver and supplier have a property such as _draggable, and the value of receiver._draggable is false (and in fact, it is)? The receiver's value value would be replaced by the supplier's value, regardless of the overrides parameter variable.

Properties that evaluate to false in a boolean context are: false, undefined, 0, NaN, null, or the empty string (§9.3 ToBoolean).

Doesn't consider shadowing

The comment on overrides: If none specified, everything in the supplier will be used unless it would overwrite an existing property in the receiver.. If the receiver does not have the property, but it can be resolved in the receiver's prototype chain (e.g. r.toString, then what? What does overwrite mean?

I would assume that overwrite would mean shadow and replace, however, the code does not completely reflect that.

replace
give a new value to a object's own property
shadow
hide a property in the prototype chain by declaring it sooner in the object's prototype chain

I'm going to get back to that. I also have to discuss the Bad browser detection part. First I'm going to discuss the in operator (§11.8.7), which could have saved augmentObject from its worst bug: Falsey values replaced by supplier, regardless of overrides.

How to use the in Operator

If augmentObject were to have used !(p in r), then false evaluating values would be preserved in the condition of overrides being false.

(function(){
var obj = { p : null };
var p = "p";
return !(p in obj);
})();
false

The in operator returns true if a property is found in the object. It considers the prototype chain using the internal [[HasProperty]] method §8.6.2.4.

Without the grouping operator (§11.1.6), the logical not operator (§11.4.9) would convert "toString" into a boolean. The value would be true. The result of the expression !true would be evaluated to false (§9.3).

(function(){
return !"p";
})();
false

The resulting in expression would evaluate to: false in {}. The in operator then converts false into the string "false". The object tries to resolve a property with the name "false".

(function(){
    return "false" in {};
})();
false

Without the in operator, we'd have no way to know if a property was present in an object. This is in's raison d'être.

(function(){
    return "undefined" in window; 
})();
true

Back to the comment on overrides: If none specified, everything in the supplier will be used unless it would overwrite an existing property in the receiver. So if the receiver does not have the property, but it can be resolved in the receiver's prototype chain (e.g. r.toString, then what? What does overwrite mean?

I'll try to show this use-case in an example. The example should help demonstrate the prototype chain (§4.2.1).

Extend and Borrow

Create a prototype link, then add extra properties to the subclass' prototype. This technique can also be used for multiple inheritance.

The constructor property is not normally enumerable, however, with the prototype inheritance approach below, the subclass gets a prototype property that is enumerable.

/**
 * Utility to set up the prototype, constructor and superclass properties to
 * support an inheritance strategy that can chain constructors and methods.
 * Static members will not be inherited.
 *
 * @method extend
 * @static
 * @param {Function} subc   the object to modify
 * @param {Function} superc the object to inherit
 * @param {Object} overrides  additional properties/methods to add to the
 *                              subclass prototype.  These will override the
 *                              matching items obtained from the superclass 
 *                              if present.
 */
extend: function(subc, superc, overrides) {
    if (!superc||!subc) {
        throw new Error("YAHOO.lang.extend failed, please check that " +
                        "all dependencies are included.");
    }
    var F = function() {};
    F.prototype=superc.prototype;
    subc.prototype=new F();
    subc.prototype.constructor=subc;
    subc.superclass=superc.prototype;
    if (superc.prototype.constructor == Object.prototype.constructor) {
        superc.prototype.constructor=superc;
    }
    
    // This has potential to lose the constructor property.
    if (overrides) {
        for (var i in overrides) {
            subc.prototype[i]=overrides[i];
        }

        YAHOO.lang._IEEnumFix(subc.prototype, overrides);
    }
},

Problem

When a subclass is created. The constructor property will be enumerable.

When an instance of any subclass is passed in as overrides, the constructor property (which is now enumerable), is exposed in the for in loop, and is then given to the new subclass' prototype.

function Animal(){}
function Bird() {}

function Duck(){}
function Mallard(){}

YAHOO.lang.extend(Bird, Animal);
YAHOO.lang.extend(Mallard, Duck, new Bird);

// The constructor of a Mallard should be Mallard.
(new Mallard).constructor === Bird; // true. This is a bug. 

Solution

Move the constructor property assignment below the for in augmentation loop.

Note on JavaScript Libraries

There are significant bugs in YUI core. YUI is one of the better popular libraries. As a "state of the art" library, it's pretty bad.

Next in this tutorial: