DontEnum ]DontEnum ]
]
Borrowing properties is adding properties of a supplier object as properties to a receiver object.
This is usually done using a for in loop.
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.
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.
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.
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] |
| Internet Explorer | Mozilla | Opera* | Safari 2 | Safari 3 |
|---|---|---|---|---|
| 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.
YAHOO.lang.augmentObjectoverrides is falseoverrides 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).
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.
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.
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).
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); } },
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.
Move the constructor property assignment below the for in augmentation loop.
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: Shadowing Properties in the Prototype Chain