DontEnum ]DontEnum ]
]Array?
It would be perfectly valid to store functions in an indexed Array.
var WidgetMethods = [
[ "toString", function() { return this.id; } ]
[ "valueOf", function() { return this.index; } ]
];
Iterate over the array, borrowing methods:
var r = {};
for( var i = 0, len = WidgetMethods.length; i < len; i++ ) {
var kvPair = WidgetMethods[ i ];
r[ kvPair[0] ] = kvPair[ 1 ];
}
Iterating over an Array is safer than using for in.
Numbers won't normally exist in the prototype chain, e.g. Array.prototype['12'] = "gotcha"; and
Internet Explorer is not known to skip any numbers.
It's pretty likely that your array won't want to call stored methods
on the Array object itself, but will probably want to use
the methods on another object. This brings up another topic, called
execution contexts, which I'll discuss next.
How to enumerate over properties cross-browser?
The problem with the previous borrowing solution, Borrow Properties Ignored by IE and Other Browsers,
is that it addresses enumeration
only for the problem of borrowing. It does not expose the loop body. The problem
with storing functions in an Array is that they can't be
used as object methods until they are attached to an Object.
Object.toArray(object) returns the enumerable
properties of the object in an Array. It is then up
to the developer to iterate over that Array.
It is impossible for a script to
determine if the constructor property of an Object should be enumerable in IE.
For cross-browser consistency, this
function excludes a constructor property in the returned
Array, even in cases when it should be enumerable.
(function(){ /** * Returns the properties of an object in a sorted Array. * uses hasOwnProperty on the object, so prototype is ignored. * Does not copy a property named constructor. If a constructor property * must be added to the array, then that must be done explicitly, by the user. * @param {Object|Function} object's own properties are added to an array of Entry objects. * @return {[Entry]} an array of Entry objects. * * an Entry has the following properties: * - key * - value * and the following prototype methods * * - toString - the value of the key + the String value of the value * - valueOf - the value of the key, to allow for sorting. */ Object.toArray = function toArray( object ) { var isFunction = typeof object == "function" && typeof object.call == "function" && object != object.constructor.prototype; if(typeof object != "object" && !isFunction) throw new TypeError("Object.toArray, Incompatible object: " + typeof object); /** * @constructor * @private */ function Entry(k, v) { this.key = k; this.value = v; } Entry.prototype = { toString : function() { return "{"+this.key + ", " + String(this.value)+"}"; } ,valueOf : function() { return this.key.valueOf(); } }; var entrySet = []; for( var prop in object ) { if(!object.hasOwnProperty( prop ) ) continue; // User must explicitly copy |constructor| over. if( prop == "constructor" ) continue; entrySet.push( new Entry( prop, object[prop] ) ); } // JScript goes wrong here. var objectDontEnums = [ // For "constructor", the user must explicitly copy it over. // "constructor" "toString" ,"toLocaleString" ,"isPrototypeOf" ,"propertyIsEnumerable" ,"hasOwnProperty" ,"valueOf" ]; // Make sure it's a real Function by checking |call| method. Also, filter out // case where object may be Function.prototype. if(isFunction) { objectDontEnums.push("call", "apply", "prototype"); } // Assume that if an enumerable toString is not enumerated, the browser generally // ignores DontEnum. var needsHelp = true; for(var x in {toString:1}) needsHelp = false; if(needsHelp) { for( var i = 0, len = objectDontEnums.length; i < len; i++ ) { var prop = objectDontEnums[ i ]; if(object.hasOwnProperty(prop)) entrySet.push( new Entry( prop, object[prop] ) ); } } // linear_b and IE go wrong here. else if(isFunction && object.propertyIsEnumerable && !object.propertyIsEnumerable("prototype")) entrySet.push( new Entry( "prototype", object.prototype ) ); return entrySet.sort(); }; //-------------------------------------------------------------------------------- // A test object var obj = { constructor : function() { return 0; } ,toString : function() { return "1"; } ,valueOf : function() { return 2; } ,toLocaleString : function() { return "3"; } ,prototype : function() { return "4"; } ,isPrototypeOf : function() { return 5; } ,propertyIsEnumerable : function() { return 6; } ,hasOwnProperty : function() { return 7; } ,length: function() { return 8; } ,unique : function() { return "9" } }; // A test function function fork(){} fork.prototype = function() { return 0; }; fork.call = function() { return "1"; }; fork.apply = function() { return 2; }; // getResultData Uses our test objects' methods to generate a result string. function getResultData(arr) { var r = []; for(var i = 0, len = arr.length; i < len; i++) { r.push( arr[i].value() ); } return r.sort().join(""); } return getResultData( Object.toArray(obj) ) +", " + getResultData( Object.toArray(fork) ); })();
Should be:123456789, 012
| Browser: | Internet Explorer | Mozilla | Opera | Safari 2 | Safari 3 |
|---|---|---|---|---|---|
| Result: | 123456789, 012 | 123456789, 012 | 123456789, 012 | 123456789, 012 | 123456789, 012 |
Useful and working correctly for IE. Unfortunately, the performance time has more than doubled. This is one unfortunate consequence of having to support Internet Explorer.
The other downside is that the user of this
function must explicitly copy a constructor
property. This will probably be a rare fringe case requirement.
Function Object.toArray works on Function and Object types.
This covers most general use-cases. I could have
created a separate function for Function.toArray, but would probably have ended up reusing a fair amount of code.
The this value in a function execution is the object that
the function was bound to.
If the this value provided is not an object,
the global object is used.
The caller's type must conform to what the
thisArg expects. If it does not, a
TypeError might be thrown.
thisArg Throws TypeError
It may seem funny that toString, a typeof function,
cannot, in some cases, be called.
We can show this in an example:
(function(){ var x = Function().toString; // Let's try calling our saved toString method. try { // This should throw a TypeError, see Bugzilla Bug 395587. x.call(); } catch(e) { alert(e.name + " " + e.message ); } })();
In the example above, we know that x is function, but we don't know what that function should be bound to;
what its thisArg type must be. This creates the problem seen above.
The same TypeError should also be present with the following code, taken from IE Blog:
function GeneralFunctionPointerMagic() {
var myElement = document.getElementById("myElement");
// This creates our function pointer and involves a look-up + creation
var funcAppendChild = myElement.appendChild;
// Author Comment: This statement is false.
// Calling this is just like any other function pointer
// Note this is a direct invoke with only a local variable look-up
// to find funcAppendChild, with no IE DOM look-ups
funcAppendChild(childElement);
}
Microsoft advocates this as a performance technique: IE + JavaScript Performance Recommendations - Part 1.
I added the comment This statement is false
. The function call is not
"just like any other function call." It's a call to a method that always
executes in the context of the node it was bound to (a useful feature,
actually). In fact,
appendChild is
not even a function. The approach is not guaranteed to work. In all other browsers,
it throws the expected TypeError.
This problem should be easy to avoid with careful planning. JavaScript will let you borrow a method and won't complain,
but will throw a TypeError upon invocation.
It would be nice to have a method Function.prototype.isCompatible( object ),
or Function.prototype.bind( object ), which would throw an Error if the bind was not
legal.
Next in this tutorial: Object.prototype.propertyIsEnumerable