DatedCard ExampleThere is one more example that tests the code above, borrowing with shadowing and replacing. It is important to make sure these features actually work.
Had ECMAScript provided an Enum construct, the whole deck could have been more consisely written entirely, and taken less space (java example). Enums are also safer than primitives. EMCAScript has no such constructs.
I create two classes, DatedObject, and Card.
Then I make a third class by merging the to classes into one, DatedCard.
DatedCard borrows from DatedObject and then Card, with shadow=true
for both operations.
There are no subclasses; no hand-wired prototype inheritance. Instead, the constructor functions DatedObject
and Card are called in DatedCard constructor. The effect this has is instance properties
of each are added to the object, in respective order, and replacing/shadowing any existing instance properties of
the same name.
(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'); function DatedObject(dateCreated) { this._dateCreated = dateCreated; } DatedObject.prototype = { getDateCreated : function() { return new Date(this._dateCreated).toString().replace(/\(.*/, ""); } ,getTimeCreated : function() { // ES really should have a native DateFormatter class. return this._dateCreated.toLocaleTimeString(); } ,toString : function() { return "DatedObject" + this.getDateCreated(); } }; Card = function Card(rank, suit) { this.rank = rank; this.suit = suit; }; // Really could use an Enum here. Card.Ranks = { "ACE" : { valueOf : function() { return 1;} ,toString : function() { return "Ace";} } // Imagine more ranks... }; Card.Suits = { "SPADES":{ valueOf : function() { return 4;} ,toString : function() { return "Spades";} } // Imagine more suits. }; Card.prototype = { toString : function() { return this.rank.toString() + " of " + this.suit.toString(); } ,toLocaleString : function () { // L10N in JavaScript needs improvement. // Note: Safari 2 does not support Object.prototype.toLocaleString return this.suit.toString()+ " \u306e " + this.rank.toString(); // JA } ,valueOf : function() { return this.rank + this.suit; } }; //---------------------------------------------------------------------------------------- function DatedCard(s, r, d) { Card.call(this, s, r); DatedObject.call(this, d); }; Object.borrow( DatedCard.prototype, Card.prototype, true ); Object.borrow( DatedCard.prototype, DatedObject.prototype, true ); // Shadow, don't replace // Our key card. var dc = new DatedCard(Card.Ranks.ACE, Card.Suits.SPADES, new Date()); var o = { toString : function() {return "DC"; } }; // toString is in dc's [[Prototype]] // it won't be borowed here: Object.borrow( dc, o, false, true ); var msgs = [dc.toString()," ------------------"] .concat("rank: " + dc.rank, "suit: " + dc.suit, "toLocaleString(): " + dc.toLocaleString(), "valueOF() : " + dc.valueOf(), "getTimeCreated(): " + dc.getTimeCreated(), "getDateCreated(): " + dc.getDateCreated(), " ------------------" ); msgs.push("(assign to dc.toString)..."); dc.toString = function() { return "I am dc."; }; msgs.push("toString(): " + dc.toString()); msgs.push("borrow (shadow=true, replace=true)..."); Object.borrow( dc, o, true, true ); // Now we can replace; we have it on the instance. msgs.push("toString(): " + dc.toString()); msgs.push("(delete dc.toString)..."); delete dc.toString; msgs.push(" ------------------",dc.toString()); var expectedEl = document.getElementById("card-expected"); Template.evalTemplate("card-expected", ["DatedObject", DatedObject, "dc", dc]); return msgs.join(String.nl); })();
Ace of Spades
------------------
rank: 1
suit: 4
toLocaleString(): Spades の Ace
valueOF() : 5
getTimeCreated(): ${DatedObject.prototype.getTimeCreated.call(dc)}
getDateCreated(): ${DatedObject.prototype.getDateCreated.call(dc)}
------------------
(assign to dc.toString)...
toString(): I am dc.
borrow (shadow=true, replace=true)...
toString(): DC
(delete dc.toString)...
------------------
Ace of Spades
| Browser: | Internet Explorer | Mozilla | Opera |
|---|---|---|---|
| Result: | Ace of Spades ------------------ rank: 1 suit: 4 toLocaleString(): Spades の Ace valueOF() : 5 getTimeCreated(): 9:49:25 PM getDateCreated(): Thu Sep 27 21:49:25 PDT 2007 ------------------ (assign to dc.toString)... toString(): I am dc. borrow (shadow=true, replace=true)... toString(): DC (delete dc.toString)... ------------------ Ace of Spades | Ace of Spades ------------------ rank: 1 suit: 4 toLocaleString(): Spades の Ace valueOF() : 5 getTimeCreated(): 9:25:34 PM getDateCreated(): Thu Sep 27 2007 21:25:34 GMT-0700 ------------------ (assign to dc.toString)... toString(): I am dc. borrow (shadow=true, replace=true)... toString(): DC (delete dc.toString)... ------------------ Ace of Spades | Ace of Spades ------------------ rank: 1 suit: 4 toLocaleString(): Spades の Ace valueOF() : 5 getTimeCreated(): 9:27:32 PM getDateCreated(): Thu, 27 Sep 2007 21:27:32 GMT-0700 ------------------ (assign to dc.toString)... toString(): I am dc. borrow (shadow=true, replace=true)... toString(): DC (delete dc.toString)... ------------------ Ace of Spades |
| Browser: | Safari 2 | Safari 3 |
|---|---|---|
| Result: | Ace of Spades ------------------ rank: 1 suit: 4 toLocaleString(): Spades Ace valueOF() : 5 getTimeCreated(): 12:05:19 AM PDT getDateCreated(): Mon Oct 08 2007 00:05:19 GMT-0700 ------------------ (assign to dc.toString)... toString(): I am dc. borrow (shadow=true, replace=true)... toString(): DC (delete dc.toString)... ------------------ Ace of Spades | Ace of Spades ------------------ rank: 1 suit: 4 toLocaleString(): Spades の Ace valueOF() : 5 getTimeCreated(): 21:50:44 getDateCreated(): Thu Sep 27 2007 21:50:44 GMT-0700 ------------------ (assign to dc.toString)... toString(): I am dc. borrow (shadow=true, replace=true)... toString(): DC (delete dc.toString)... ------------------ Ace of Spades |
A few annoyances with Safari 2: Does not support
Object.prototype.toLocaleString, throws ParseError when encountering a
FunctionExpression in an object literal. Both problems are fixed in Safari 3.
Object.borrow solves the problem of enumerating when borrowing. It does not, however
allow for anything other than borrowing to occur in the loop body. A more abstract solution
is required.