Thursday, 23 December 2010

Say what?

Something that seems to come up a lot in JavaScript work is the need to figure out what kind of thing you're dealing with — is it a string? a number? a Date? And JavaScript has various features to help you do that, but there are some "gotchas" to watch out for. In this post I'll talk about various strategies for figuring out what things are.

Basically, you have four tools you can use, each of which has its place:

  • typeof
  • instanceof (and obj.constructor)
  • Object.prototype.toString
  • Learn to stop worrying and love the duck

Let's look at each of them:

typeof

typeof is fast but fairly limited. It basically tells you whether something is a primitive or an object, and if it's a primitive it tells you what kind of primitive it is, but for all objects other than functions, it just says "object". So:

alert(typeof "testing");             // "string", but: 
alert(typeof new String("testing")); // "object"

(And similar for numbers and booleans.) But it's useful for, say, determining if something is a function:

if (typeof x === "function") {
// Yes it is
}

typeof's chief advantage is speed.

instanceof (and obj.constructor)

instanceof sort of picks up where typeof leaves off. It's good for checking whether something is a specific kind of object, e.g.:

var t = new Date(...); 
alert(a instanceof Date); // "true"

Specifically, it checks if the object's constructor strictly equals (===) a given constructor function. You can do this yourself if you like, the constructor of an object is available via its constructor property, and so these are nearly equivalent statements:

var d = new Date();
alert(d instanceof Date); // "true"
alert(d.constructor === Date); // also "true"

...except that if you've explicitly assigned a different value to the constructor property, instanceof checks the actual underlying constructor whereas the second statement above would be fooled — whether that's good or bad depends on your reasons for setting the property!

You do have to be careful with instanceof (and checking constructor) in some edge cases, though, particularly if you're writing a library and so have less control / knowledge of the environment in which it will be running. The issue is that if you're working in a multiple-window environment (frames, iframes), you might receive a date d (for instance) from another window, in which case d instanceof Date will return "false" — because d wasn't created by that window's Date, it was created by a different window's Date. (You don't actually need multiple windows for this to come up, but that's the most common way in browser apps. You have to be doing esoteric things for it to come up otherwise.) And in most cases you don't care, you just want to know whether it has all the Date stuff on it so you can use it.

Object.prototype.toString

Calling Object.prototype.toString is slower than typeof or instanceof, but more useful for differentiating what kind of object you have — but sadly, only if the object is one created by one of the built-in JavaScript constructor functions like Date or String, not our own constructor functions. The return value of the Object prototype's toString function is defined in the standard for all the built-in constructor functions (Array, Date, etc.): It returns "[object ___]" where ___ is the constructor function name. So:

var what = Object.prototype.toString;
alert(what.call(new Date())); // "[object Date]"
alert(what.call(function(){})); // "[object Function]"
alert(what.call([])); // "[object Array]"

...etc. Note that this is very different from just calling toString on the object itself; the object may have (probably does have) an overridden toString. That's why we explicitly use Object.prototype.toString above, to make sure that we're calling the function with the well-defined behavior. (Remember, these things are just functions, not methods.) Object.prototype.toString works on primitives too, because it coerces its argument into an object before checking. So if you call it with a string primitive, you get "[object String]" (and "[object Number]" for numbers, etc.).

The chief advantage of this is that it doesn't care what window the object came from; referring back to our Date object d from another window, we'll get "[object Date]" regardless.

But sadly for our own constructor functions, all we get is "[object Object]":

var what = Object.prototype.toString;
alert(what.call(new MyNiftyThing("foo"))); // "[object Object]"

Ah, well...

At one point I was tempted to wrap all of the above (plus support for my own object hierarchy) up into an uber-function that definitively figured out what the thing was. Then I thought: Enh, maybe I should just use the right tool in each given situation.

Speaking of which, our last tool:

Learn to stop worrying and love the duck

When reaching for instanceof or typeof or whatever, ask yourself: Do you really care? How 'bout looking to see if the object seems to have the things on it you need (feature detection) rather than worrying about what it is? This is usually called "duck typing," from the phrase "If it walks like a duck and talks like a duck..." Sometimes, obviously, you do care, but if you get in the habit of asking yourself the question, it's interesting how frequently the answer is "Actually, no, I don't care whether that's a Foo; I just care that it has X on it" or "...I just care that it works if I pass it into getElementById" (that latter case being, basically, it's a string or its toString does what you need). I find myself doing a lot less worrying about types and just getting on with the job these days. I've learned to love the duck.

And always remember, be kind to your web-footed friends; a duck may be somebody's mother.

Happy hols, y'all.

No comments: