Type Checking in JavaScript Wednesday, 10 October 2007

So how do you tell if an object is a function?

What's wrong with typeof?

typeof o == "function";

Some browsers have unexpected behavior when using the typeof operator with Host objects.

For example, in Safari, typeof aNodeList == "function" bug 14547.

Mozilla returns "function" for typeof on RegExp objects (bug 14547), and object elements (bug 268945, bug 296858).

The instanceof Operator

The more flexible alternative to typeof is instanceof.

    return it instanceof Function;

Unfortunately, instanceof will not return the desired result across frames. Each window has a different global object, so Function in frame 1 is not Function in frame 2.

When working across frames, the constructor property and isPrototypeOf will have the same problems as instanceof.

For example, an iframe with a function f, and the following code in the parentWindow:

var i = document.getElementsByTagName("iframe")[0];
var iframeWindow = i.contentWindow;
iframeWindow.f instanceof Function; // false
typeof iframeWindow.f == "function"; // true;

It should be easy to know what an object's type is. Why does JavaScript make this so hard?

What Do the Libraries Do?

Dojo had the idea of depending on a combination of the userAgent string, the return value of the object's toString method, and implicit type conversion that happens with the Equals Operator §11.9.3.

if(dojo.isBrowser && dojo.isSafari){
 // only slow this down w/ gratuitious casting in Safari since it's what's b0rken
  dojo.isFunction = function(/*anything*/ it){
    if((typeof(it) == "function") 
      && (it == "[object NodeList]")){ return false; }
  return (typeof it == "function" || it instanceof Function); // Boolean
  }
}else{
  dojo.isFunction = function(/*anything*/ it){
    return (typeof it == "function" || it instanceof Function); // Boolean
  }
}

This is to address Safari's unreliable results when using the typeof operator on one particular host object.

The function will fail in Safari 3 when passed an HTMLCollection (try typeof document.links). It will fail for any other object that Safari thinks implements call.

The problem is not that dojo forgot to check for HTMLCollection. Web developers should not need to know such obscurities.

jQuery : isFunction

The jQuery code stumbles on this problem:

// This may seem like some crazy code,
// but trust me when I say that this
// is the only cross-browser way to do this. --John

  isFunction: function( fn ) {
    return !!fn && typeof fn != "string" && !fn.nodeName && 
      fn.constructor != Array && /function/i.test( fn + "" );
  },

Lets break down this crazy code to see what it's doing.

( !!fn ).......................convert to boolean value. null, et c are out
( typeof fn != "string" )......not a string value (String objects pass here) 
( !fn.nodeName )...............Not an object with a truthy nodeName property
( fn.constructor != Array )....Not an Array object constructed in this frame
( /function/i.test( fn + "" )..toString contains "function"

Does it work?

// Case 1: testObj is not a function. 
var testObj = {
    toString: function() {
        return "This object contains only one function."
    }
};

// Case 2: testString is not a function.
var testString = new String("hey! This is a String, not a function!");

// Case 3: WidgetFactory is a function.
function WidgetFactory( id ){ }
WidgetFactory.toString = function() { return "WidgetFactory"; };

// Case 4 : An Array in the frameWindow is not a function.
// var someArray = [ function(){}, "Simplify functional testing." ];

// Here's the test:
var frameWindow = document.getElementsByTagName("iframe")[0].contentWindow;
isFunction( testObj ); // true.
isFunction( testString ); // true.
isFunction( WidgetFactory ); // false.
isFunction( frameWindow.someArray ); // true, contains "function" (twice).

Doesn't work that well, does it?

Running code doesn't lie. All four tests failed.

The jQuery function will give the correct result sometimes, but not in any of the cases above. This is because jQuery is relying on the result from the function's toString.

These cases are quite obvious and likely cases where the above method will fail. Nothing tricky about it.

The value returned by toString should not generally be relied upon, and especially not with a Host object.

JavaScript forces developers to rely on toString with Date or Number objects. This is an API design of the language that goes against convention. Such design should not influence a JavaScript library author's design decisions.

The disadvantage of specifying the format of the toString return value is that once you've specified it, you're stuck with it for life.—Joshua Bloch, quote from Effective Java

Method toString should not, generally speaking, be relied upon. Instead, toString should be used for diagnostic messages.

String Objects are Not string Values

Web developers sometimes get confused with string values and String objects. My tutorial on how property access operators work, clearly explains this fundamental concept.

Here's another example from Dojo.

dojo.isAlien = function(/*anything*/ it){
  // summary: 
  // Returns true if it is a built-in function or some other kind of
  // oddball that *should* report as a function but doesn't
  if(!it){ return false; }
  return !dojo.isFunction(it) 
    && /\{\s*\[native code\]\s*\}/.test(String(it)); // Boolean
}

Besides conjuring up images of little green men, function isAlien returns true for anything that isFunction returned false for and also contains "[native code]" in the object's string value. The intent seems to be to check if a function might be a function even when typeof failed.

I have a very hard time believing that any unmodified built-in function does not return "function" for typeof. The comment needs more detail. It would be interesting to see the dojo comment proven true. At least they put an explanatory comment in.

The comment implies that a built-in function might not return "function" but does not provide a case where this is true. The code is based on that assumption and assumes that such an object's String value will contain "[native code]".

This approach suffers from the same problems that jQuery's isFunction suffered from. If any object's string value contains "[native code]", isAlien will return true. Granted, "[native code]" is less common than "function", but buying vowels isn't the answer.

Function isAlien provides a way for non-callable objects to slip through as functions. It's inclusive, like the jQuery function, and just as dangerous. It does this by relying on [native code] making deductions about the Object based on its toString's return value.

Return values from toString should not, in general, be relied on

If it walks like a duck and quacks like a duck, it must be a duck.

How to Determine if an Object is Really a Function

Functions are unique in a few ways. The return value for toString is not one of them.

It seems safer to introspect the object based on a unique characteristic.

Update - Oct 13

A solution is needed. Here is one more offering, introspecting the constructor property, accompanied with a testcase.

function isFunction(fn) {
  if(typeof fn != "function") return false;
// Now check the constructor property. 
  if(typeof fn.constructor != "function") return false;
// If constructor is Function, then constructor.prototype will have call.
  return fn.constructor.prototype.hasOwnProperty( "call" );
}

That's some ugly looking code, huh? Who has a better one?

Try to provide case where the testcase will fail.

We can make more functions for isArray, et c. What a pain in the ass. This should be easy. JavaScript makes it hard.

Type Checking - the Status Quo

JavaScript needs a way to perform equivalence check. This might be helpful for cross frame issues.

JavaScript provides instanceof and typeof, but neither are sufficient

instanceof fails across frames. It doesn't work with primitives, e.g. "foo" instanceof String is false.

The typeof operator has a limited number of return types. typeof doesn't support all of the built-ins properly (e.g. Array, Error, Date, and null are all "object") and doesn't support any user-defined constructors. Typeof is allowed to return an implementation-dependent value for host objects. Safari conforms, but in an annoying way that causes problems.

Type Result
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Object (native and doesn't implement [[Call]]) "object"
Object (native and implements [[Call]]) "function"
Object (host) Implementation-dependent

Internet explorer includes an additional "unknown".

Typechecking is an area that JavaScript, as a language, needs to improve.

Update...

Brendan just posted on the ES4 List:

The is operator tests universal or Platonic type, which involves shared, immutable type descriptors that do not vary across windows or frames. So

(it is Callable)

That looks very useful. Immutable objects are powerful building blocks for other object types. Looks like there might be the possibility of other callable types (or subclass of Function) in ES4.

Technorati Tags:

Posted by default at 4:57 PM in JavaScript

 

Comment: Dave at Tue, 27 Nov 7:35 PM

> "I have a very hard time believing that any unmodified built-in function does not return "function" for typeof."

In IE7, type this into the Address bar:

javascript:alert(typeof document.getElementById)

Now type this:

javascript:alert(String(document.getElementById))

Oh, *that's* where the dojo guys got the "[native code]" from! Quack.

It is useful to have a way to know whether something is callable code. Your proposed isFunction does not do that.

Comment: david_kw at Tue, 27 Nov 8:42 PM

Try this in IE:

<input id="t"/>
<script>
var t = document.getElementById("t");
alert(t.focus);
t.focus();
</script>

Comment: david_kw at Tue, 27 Nov 10:55 PM

I'm having trouble commenting. Suppose I just say that in IE input.focus() isn't typeof 'function' and is a built-in function.

Comment: Garrett at Sun, 2 Dec 11:45 PM

Hi David,

getElementById is a DOM method that operates on a Document -- not a built-in function.

But maybe if you think it is, then that's what the Dojo guys were thinking, too. Method input.focus is not a built-in function any more than getElementById is.

If you're trying to call me a quack, well, that is not acceptable here. That's not what this blog is about.

Comment: Matt Kruse at Sun, 2 Dec 11:50 PM

The test cases for jQuery don't fail for me, using jQuery's method as posted. Are you sure you tested correctly?

I'm working on a more robust solution myself. So far I've covered all the jQuery test cases, covered a few cases that actually fail with jQuery, and i'm trying to make sure I cover your test cases as well. I just can't seem to make them fail with jQuery ;)

Comment: Garrett at Mon, 3 Dec 12:01 AM

Hey Matt,

For some reason, I couldn't get your comment to be approve, so I reposted it.

Sure, I can make a quick ad-hoc test using Firebug using the snip above. I don't know if jQuery has been amended, but last time I checked it was the same.

====================================
var testObj = {
toString: function() {
return "This object contains only one function."
}
};

// The jQuery isFunction
function isFunction( fn ) {
return !!fn && typeof fn != "string" && !fn.nodeName &&
fn.constructor != Array && /function/i.test( fn + "" );
}

isFunction(testObj); // This returns true, (should be false).
====================================

I don't usually have to worry about passing | document.images | to a function and getting an error, though. I almost never use the faux-overloading approach. I will probably like to use the multimethods in ES4, though.

Comment: Dave at Thu, 3 Jan 4:22 PM

Matt, I just got back and saw your message. I wasn't saying you were a quack, I was saying it was a form of duck typing. If it's callable like a function, then it *is* a function. Sure it doesn't have .apply and .call methods but in many/most cases that doesn't turn out to be an issue. Perhaps the name isCallable would have been a better choice for all of these.

Sorry for the misunderstanding.

Comment: kangax at Tue, 1 Apr 12:30 AM

Hmm, how come:
isFunction(Function('foo', 'return foo;')); // => false

Comment: Garrett at Tue, 1 Apr 3:09 AM

Which isFunction? The jQuery one, or the one that checks for - call - property?

And which browser?

I get - true - for both in Firefox:-

function isFunction(fn) {
if(typeof fn != "function") return false;
// Now check the constructor property.
if(typeof fn.constructor != "function") return false;
// If constructor is Function, then constructor.prototype will have call.
return fn.constructor.prototype.hasOwnProperty( "call" );
}
isFunction(Function('foo', 'return foo;'));

result: true

Comment: kangax at Wed, 2 Apr 11:30 PM

Garrett,

It does indeed return true. I probably tested the wrong isFunction accidentally : /

The case where it would fail is:

function foo(){}
delete foo.constructor.prototype.call
isFunction(foo); // => false

Though, this is obviously a nonsense case.

- kangax

 

*AnimTree
*Tabs
*GlideMenus
*DragLib