Enumeration and Object Oriented JavaScript

 

*AnimTree
*Tabs
*GlideMenus
*DragLib

Borrowing Using Library Code

Example of Shadowing a Property in the Prototype Chain

  1. Create a Panel class whose prototype has a type property that is truthy.
  2. Create a Menu class that extends Panel.
  3. Create a Draggable class with a type property.
  4. Create a draggable menu by mixing (window.__r) in instances of Panel and Draggable.
(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].

})();
(function(){
// Grab the previous example's source.
evalTextContent('dont-enum-aug-hidden-source');

function Panel( ){ }
Panel.prototype.type = 1;
Panel.prototype.dockable = false;

function Menu() {}
Menu.prototype = new Panel( );
Menu.superclass = new Panel;
Menu.prototype.show = function() {
    return( this.constructor.prototype.type );
};

function Draggable ( id, type ) {
   this.id = id;
   this.type = "horizontal-drag";
   this.dockable = true;
   this.drag = function(){ return (this.type); };
};

Animatable = {
    valueOf : function() {return 3;}
};

window.__r = new Menu;
var hasTypeBefore = window.__r.hasOwnProperty('type');
var hasToStringBefore = window.__r.hasOwnProperty('toString');

YAHOO.lang.augmentObject(window.__r, new Draggable, true)
YAHOO.lang.augmentObject(window.__r, Animatable);

var hasTypeAfter = window.__r.hasOwnProperty('type');
var hasToStringAfter = window.__r.hasOwnProperty('toString');

return [
 window.__r.drag( )
     ,hasTypeBefore
     ,hasTypeAfter
     ,window.__r.valueOf() // Should the valueOf be taken from Animatable?
     ,hasToStringBefore
     ,hasToStringAfter
 ].join(String.nl); 
})();
Result Expected Yahoo Expects
  horizontal-drag false true [object Object] false false horizontal-drag false true unclear false unclear
Browser: Internet Explorer Mozilla Opera (as IE*) Safari 2 Safari 3
Result: horizontal-drag false true 3 false false horizontal-drag false true [object Object] false false horizontal-drag false true 3 false false horizontal-drag false true [object Object] false false horizontal-drag false true [object Object] false false

It's difficult to say what the Expected Result should be. YUI's comments are unclear and the code is contradictory.

No property value of window.__r was replaced, or "overwritten". I will prove this with the delete operator.

Browsers that identify as IE will get valueOf copied to window.__r. Other browsers will not get valueOf copied. This is because overrides is false. Function _IEEnumFix ignores overrides.

* To Change Opera's User Agent: Opera > Quck Preferences > Mask as Internet Explorer

Shadowed Properties

Do we consider the receiver's [[Prototype]], or just the receiver? What about the supplier's prototype?

To understand the difference, it is necessary to understand the prototype chain. window.__r.type is resolved in window.__r's prototype chain. The value is 1.

Before borrowing from Draggable, window.__r does not have a type property on its self. The type property exists in the [[Prototype]].

A new property is created on __r, shadowing the existing type property in the prototype. This can be proven by calling delete window.__r.type.

(function(){
// Grab the previous example's source.
evalTextContent('dont-enum-aug-2-source');

var result = [ "before-delete: " + window.__r.type ];
if( delete window.__r.type ) {
    result.push("after-delete: " + window.__r.type);
}

return result.join( String.nl );
})();
Result: Expected:
     
before-delete: horizontal-drag
after-delete: 1

A shadowed type property still exists. We could re-shadow it, if we wanted to, but I think once is enough to get the idea. If you have a browser that supports read/write access to __proto, you could delete that shadowed property with delete window.__r.__proto__.__proto__.type

The delete operator returns false only when the property had the DontDelete attribute. It returns true otherwise, even when nothing was deleted (even if the object does not contain the property). This is correct behavoir, as per the spec, but is counterintuitive.

(function(){
return delete ({}).nothing;
})();
true

Internet Explorer will throw an error when calling delete on a host object, even for an expando or custom property (expando can also cause memory leak problems).

(function(){
var isIE = false;
try{
    var deleted = delete window.clipboardData;
} catch( e ) {
    isIE = true;
    try {
        alert( e.name ); // IE Actually throws an error on e.name - bad variable name.
    }
    catch(ee) { // IE Actually throws an error here.
        alert( ee.name + ": " + ee.message + " \n"  );
    }
} finally {
    return isIE;
}
})();
isIE:
true

So now I've covered delete; helping to demonstrate how shadowing works.

Such misunderstandings can and should be expected for language features that are not clearly exposed to the programmer.

Now that I've explained shadowing, I'll back to where I left off with the Yahoo example.

Analysis: YAHOO.lang._IEEnumFix

Method _IEEnumFix is intended to address the JScript DontEnum bug.

A bug report was filed around January, 2007, recommending that Yahoo investigate the problem and test the solution. The problem was that Yahoo.augment had not addressed the JScript DontEnum bug.

YUI's answer is _IEEnumFix.

/**
     * IE will not enumerate native functions in a derived object even if the
     * function was overridden.  This is a workaround for specific functions 
     * we care about on the Object prototype. 
     * @property _IEEnumFix
     * @param {Function} r  the object to receive the augmentation
     * @param {Function} s  the object that supplies the properties to augment
     * @static
     * @private
     */
    _IEEnumFix: function(r, s) {
        if (YAHOO.env.ua.ie) {
            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;
                }
            }
        }
    }

The code comment states: IE will not enumerate native functions in a derived object even if the function was overridden.

Actually...

During enumeration, IE will skip any property in any object where there is a corresponding property in the object's prototype chain that has the DontEnum attribute. Native code is not a property; it's a value; the term derived has no meaning or relevance.

Overrides

_IEEnumFix does not take the overrides parameter variable. This explains the different results in IE.

Bad browser detection

The userAgent string will usually provide the correct userAgent. This addresses a browser, however, not a problem. Opera will be inflicted with the _IEEnumFix when the user preference is set to mask as Internet Explorer.

Do not fork for a browser if it can be avoided. Use feature detection when you can. Use the user agent as a last resort.

—YAHOO.env.ua documentation

Good advice.

One final oddity about the loop is the avoidance of the postfix-increment operator in favor of i=i+1. This is most likely due to paranoia that can be directly accredited to JSLint and its author who provides advice to Yahoo.

Working with such low-level problems as these is not simple.

After all this analysis, I'm going to provide what I think will address the problems and avoid the bugs.

Next in this tutorial: