My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Monday, August 03, 2009

isFunction or isIENativeFunction ?

One truly common task in JavaScript is to determinate if a specific "reference" is a Function, as we know them, or not. For those guys whose think typeof, instanceof, or Object.prototype.toString are enough, here I am with the proof things are NOT that simple, and these things are really important, business speaking, since these checks are in the core of whatever library we create or use for our Web 2.0 Application, Enterprise or not.

Different Behavior Problem: Internet Explorer


As historical black sheep in the browser panorama for its non standard JavaScript implementation, even for a simple task like the right manifestation of a property or function in core level, Internet Explorer, whatever version, will return false in every case described below:


// alert is a function, that is why I use
// this function to demonstrate
// that for IE alert is not a function
// sounds fun?

alert(typeof alert); // object

alert(alert instanceof Function); // false

alert(Object.prototype.toString.call(alert));
// [object Object]

Function.prototype.toString.call(alert);
// Error: Function expected


NO WAY, Internet Explorer think that every native function is a property with a getter that if invoked, even internally via native prototypes, returns an invokable object rather than its "call".
An invokable object is reproducible in this fake way, and it is basically a function which is an Object ... OMG, did I say function?

Do Not Give Up!


Since there is apparently no way to define if a native function is a function, jQuery library, as example, simplified its own isFunction public static callback.
The reason is ... reasonable:
.isFunction is simpler now, it no longer handles some strange edge cases (in favor of simplicity and performance)

Good stuff, simplicity and performances, plus edge case removal, is probably the right direction. But, what if we would simply like to understand if a function is actually a function?

isIENativeFunction Proposal


As I wrote in the jQuery developer mailing list, this little snippet will most likely solve every problem:

jQuery.isIENativeFunction = function(f){
// WebReflection Proposal

return !!f && // cause we want return true or false

typeof f.toString === "undefined" &&
// cause native functions do not
// contain a toString callback
// as is for every user defined
// function or object, even if deleted
// so next step is a "safe" destructuration
// assumption

/^\s*\bfunction\b/.test(f)
// cause we are looking for a function
// and IE shows them with function
// as first word. Eventually
// there could be a space
// (never happened, it does not hurt anyway)
;
};

How to test if above little monster works as expected?

alert([
jQuery.isIENativeFunction(function(){}), // false
jQuery.isIENativeFunction(alert) // true
]);

In few words with this little overhead, exactly 100 characters once minified

function isIENativeFunction(f){return!!f&&typeof f.toString==="undefined"&&/^\s*\bfunction\b/.test(f)};

we can have under control every kind of check, when and if necessary, thanks to a simple double check that 90% of cases will stop at first call for every browser (I assume we check functions most of the time) and only for IE it will call the second check when the function is a native one. If that will not be a function, well, IE and every other browser will simply return false.

// seriously, I want to know if that
// was a function ...

if($.isFunction(f) || $.isIENativeFunction(f))
f();



Not Only jQuery


What if we would like to have a portable isFunction clever enough to understand every situation only when we need it?

var isFunction = (function(toString, F){
// Another WebReflection Proposal

// test if a function is native
// simply trying to evaluate its
// original Function.prototype.toString call
function test(f, n){
// it should be a function in any case
// before we try to pass it to
// Function.prototype.toString
if(n = isFunction(f, false)){
try{
// no execution
// just an error if it is native
// every browser manifest native
// functions with some weird char
// that cannot be evaluated [native]
this.eval("(" + F.call(f) + ")");

// if eval was fine, function was
// not native
n = false;
}catch(e){};
};
return n;
};

// avoid monkey strings ...
// let the browser decide
// how Object.toString will represent it
// [object Function] 100% of the case
// but IE is always present ...
var s = toString.call(F);
try{
// native function cannot be passed
// to native Function.prototype.toString
// as scope to evaluate ... only IE, sure
F.call(this.alert);
// this function is for every other browser
function isFunction(f, n){
// the good old toString call
// if "n" argument is true
// we check if the function is native
return !n ? s === toString.call(f) : test(f, n);
};
}catch(e){
// IE party!
// even if u was defined outside
// next expression will produce undefined
// the typeof is performed in the var u
var u = typeof u;

function isFunction(f, n){
return !n ? // no native check ?
// god old toString then ...
s === toString.call(f) :
// test if IE trick proposed for jQuery
// works as expected or try
// to understand if that function
// is native via test one
(!!f && typeof f.toString === u && /^\s*\bfunction\b/.test(f)) || test(f, n)
;
};
};
return isFunction;
})(Object.prototype.toString, Function.prototype.toString);

To test above code?

//* Example
// Others | IE
alert([ // --------------
isFunction(function(){}), // true | true
isFunction(Function), // true | true
isFunction(alert), // true | false
"\n", // --------------
isFunction(function(){}, true), // false | false
isFunction(Function, true), // true | true
isFunction(alert, true), // true | true
"\n", // --------------
// all true
isFunction(function(){}) || isFunction(function(){}, true),
isFunction(Function) || isFunction(Function, true),
isFunction(alert) || isFunction(alert, true)
]);
//*/

In few words, every browser but IE will tell us if a function is a function, as is right now with the simplified version of jQuery 1.3 and many other libraries.
On the other hand, we can understand if a function is a native one with every browser.
This operation will cost more and for this reason a true, as second optional argument, is necessary to force other checks.
isFunction(f) || isFunction(f, true) should be always true in every browser if f is a function, and it will be evaluated slowly twice only by IE cause other browsers will not pass the internal isFunction check if f is not a function.

One callback to rull "function investigation cases" all? Dunno what you think, but I like it :D

4 comments:

Samer Ziadeh said...

Nice work, thanks for the info :)

Andrea Giammarchi said...

One post about performances disappeared ... something weird with blogger since yesterday, I'll manually write down later.
As answer, eval is extremely slow if you have Firebug activated.
eval is necessary to avoid global scope overrides (via Function).
At the same time, as Arial said in jQuery dev list, cases where we need to do use the true second arguments are extremely rare.
This post is more about the possibility to know, rather than unconditional usage.
XMLHttpRequest, how to know if it is native or redefined (IE6) ?
eval itself, via isIENativeFunction, is it the original one? setInterval and setTimeout, have they been redefined? etc etc ... I can't spot a case where we need the double check or when the double check plus eval is actually performed.

Anonymous said...

Hi.
IE does not provide normal way, for "isIENativeFunction". Regular Expression is not very well, for testing some object for function. What's happen if i test:

isIENativeFunction(ActiveXObject);

Or if i want to test, something like this:
var ax = new ActiveXObject('Microsoft.XMLHTTP');
isIENativeFunction(a.open); Here, comunicate with ActiveXObject and ActiveXObject doesn't have property like open, and throw Error.
David Flanagan provide one utility method.
Object.isCallable. He use forEach method ot array object, but at this moment in IE doesn't work, because Array don't have forEach :)

Andrea Giammarchi said...

I think you missed the next post about this subject ...