Just what is this Javascript object you handed me?
Modern Javascript “frameworks” like Prototype, jQuery, and Dojo compete to provide flexible, expressive tools, which is a great thing for Javascript developers. It’s truly astounding how effective these relatively simple and concise tools can be.
One of the challenges faced by toolkit designers stems from the malleability of the Javascript environment - the same malleability that makes the tools possible in the first place. In type-happy (I could have written “stodgy”) languages like Java, the question, “what is this thing?” that code behind an API might ask is always easy to answer; in fact as a Java programmer you don’t really ask the question much unless you’ve done things pretty badly. In Javascript, however, there’s never a good answer. Well, there is - the thing is an Object - but it doesn’t do you much good.
So anyway, coding away the other day I came across an interesting bug, one that turns out to be shared by Prototype, jQuery, and Dojo (and maybe others). It was a case of the toolkit code doing something to check to see if a thing passed through an API was of a particular type so that some work could be done with it. In this case, the toolkit code (Prototype) needed to know whether a value was an Array, because if so then something different had to be done than if the value were a simple scalar. Instead of approaching that situation with a pragmatic, dynamic “duck typing” technique, the code took an approach that a Java programmer would recognize.
Specifically the fault was in code that transformed a “Hash” object into an HTTP query string. The client code has a property list of parameter names and values, which is to be used in a GET URL or an XmlHTTPRequest. The Prototype service has to deal with the fact that a single parameter name may be associated with a list of values. (Recall that an HTML form can have more than one input field with the same name.) The Prototype convention here is that the “toQueryString” routine looks for parameters in the Hash object whose values are arrays:
// this is 1.5.1 code - it's different in 1.6
// but the problem remains (for now)
toQueryString: function(obj) {
var parts = [];
parts.add = arguments.callee.addPair;
this.prototype._each.call(obj, function(pair) {
if (!pair.key) return;
var value = pair.value;
if (value && typeof value == 'object') {
if (value.constructor == Array) value.each(function(value) {
parts.add(pair.key, value);
});
return;
}
parts.add(pair.key, value);
});
return parts.join('&');
},
// ...
(The “addPair” routine that’s hooked up to the return-value array “parts” just builds a “name=value” string with appropriate URL encoding.) See how the code works? There in the loop code where it handles each key/value pair in the Hash object, it checks each value to see if it’s type is “object”. (The Javascript “typeof” is pretty lame.) If it’s an object, then a further test is made to see if the value is an array. That test compares the “constructor” attribute of the value object to the global Array function.
What’s the problem with this? The code is doing something that seems reasonable: if the constructor of the object is the Array function, then it’s an array, right? Sure, this is Javascript, so if the client code has done something stupid like reassign the “constructor” attribute or even reassign the global variable name “Array”, it’ll break, but that would be my fault. Setting that aside, what could possibly go wrong?
First I’ll be philosophical: does that code really need to know for sure that the value was constructed by the Array function? All it really does when it determines that the value is an array is take advantage of the “each” function it expects to find there. I think that a pure (funny word in this case) duck-typing approach would be to check to see if the value object has an attribute called “each” that references a function.
Now on to the practical issue here, the reason that the code is buggy as written. The subtle assumption made in the code is that every array object will have been constructed by the function pointed to by the global variable “Array”. Conversely, it assumes that any object not constructed by the function pointed to by the global variable “Array” must not be a real array. OK then, outside of shenanigans that break things on purpose (reassigning “Array”), how is that assumption problematic?
Well I’ll tell you. A web application can be spread across multiple separate windows. The window objects are linked together into a single DOM graph. Code can “see” up and down the graph into other windows, and functions can be called across window boundaries. In particular, an array can be passed from code on one page into a function on another. The kicker is that every page has its own global context (which means that the term “global” is sort-of questionable), and that includes a “global” variable called “Array.” Oops.
Javascript is not Java. You can’t really put much stock in anything your code might find out about a mysterious parameter, so why compound the problem by asking indirect questions? Just because an object was not apparently constructed by the function you think has to be the one and only “right” one doesn’t mean it won’t work just fine.
This page is a simple example of the problem. Prototype will be fixed soon thanks to the attentions of world-famous Prototype contributor “savetheclocktower.” Dojo and jQuery have similar issues but I haven’t reported them yet. Specifically, jQuery uses the “foo.constructor == Array” deal all over the place. Dojo seems to use “instanceof Array”, which suffers from exactly the same problem. I don’t think that expecting the libraries to work properly on values passed across page boundaries is unreasonable.
December 8th, 2007 at 7:42 pm
jspon defines semantics for an object to declare its type. no more .prototype.constructor guessing BS.
December 8th, 2007 at 10:21 pm
base2 (http://code.google.com/p/base2/) has an instanceOf() function that solves this problem.
December 9th, 2007 at 6:22 am
@rektide + Dean
That’s nice, but I believe the authors point was, that the problem needn’t exist in the first place. It basically doesn’t make sense to rely on an objects type in Javascript.
December 13th, 2007 at 2:30 pm
To solve the array checking issue you suggest: “check to see if the value object has an attribute called “each” that references a function.”
Maybe I’m misreading the issue, but it seems that you’ll be in the same boat when it comes to determining if each is a function.
In the example code, and the discussion surrounding it, if the object has a property that is named each, rather than a method named each, then it would pass the validation that you’re suggesting, as you can’t determine the difference between a method and a property unless you then call each as a method and wrap that with a try-catch. Or is that what you’re suggesting the fix be?
December 21st, 2007 at 12:20 am
Danilo (hey man!), you can check if typeof(obj.each) == ‘function’ to figure out if each is a method.
December 21st, 2007 at 6:34 am
@Danilo: Mr. Wubben is correct - a possible “duck” test would be to check to see of the “each” attribute exists and has type “function.”
December 21st, 2007 at 8:07 am
JavaScript libraries in fact suffer a fate much larger than that when used in a multi-framed environment. Any of the following tasks won’t work cross frame:
1) Creating elements using convenience functions. e.g.,
jQuery(”").appendTo(frames[0].document.body);
2) Getting the style of an element. e.g.,
dojo.style(frames[0].document.body, “color”);
because it calls “document.defaultView.getComputedStyle” which is not the same document as the frame’s object.
3) Getting the event in IE. e.g.,
Element.observe(frames[0].document.body, f.bindAsEventListener(this));
because it returns window.event instead of frames[0].event;
December 21st, 2007 at 9:40 am
Hey Mark,
Thanks for that, I guess I was thinking along the lines of the value.constructor == Function for that check, but I stepped right over typeof when thinking about this. Thanks for the reminder Mark and BigDingus.
December 26th, 2007 at 8:34 pm
@Nathar Leichoz:
Someone correct me if I’m wrong, but couldn’t this be solved by using the “ownerDocument” property of the passed Node to determine the containing document and go from there? So this is a fixable problem, yes?
January 8th, 2008 at 9:47 pm
I came across this a few days ago, when writing the cross-window functions for a notification-window (essentially a non-blocking alert) system for our UI. My setup was slightly different - the user can specify which window they want the notification to appear in, and the framework ensures that any wrapper window will have the new notify() function (inside of which I needed to do this check) defined on it. I basically needed to see if ContainerWindow.notify == window.self.notify… but that’s the same issue you’re discussing here - Javascript considers different windows’ instances of functions from identical sources to be unique functions.
My solution, and it may have its shortcomings in the long run, was simply to compare the string-values of the functions, instead of the functions themselves. (i.e. String(ContainerWindow.notify) == String(window.self.notify) ) It worked like a charm. That may help here, as native functions could be considered to be unmodifiable without their source appearing in its string-value, at which point string-comparison would return false. The only thing I see that the string values don’t take into account is closures/scope. While I think that the situation where two constructors’ closures/scope would render them identical in string-value (while the intent of the developer is for them to be recognized as unique constructors) would seldom occur, I’ll leave it up for real-world abuse for a while, before I’m convinced either way.
April 22nd, 2008 at 8:03 pm
I would feel better with:
typeof value === ‘object’
June 24th, 2008 at 9:23 am
Hi,
The Duck Typing way to solve this is to use the object as if it were an array and catch tne exception if it is not then if it triggers the “I’m not sufficiently an Array” exception, use it as a scaler.
- Paddy.
July 28th, 2008 at 1:13 pm
@Chad:
I really don’t think you should feel any better with typeof value === ‘object’ instead of typeof value == ‘object’, because typeof is guaranteed to return a string, while the left hand side is obviously also a string, so a type mismatch is pretty much impossible here.
August 11th, 2008 at 10:59 am
Utility function to find out the actual class names of objects:
http://magnetiq.com/2006/07/10/finding-out-class-names-of-javascript-objects/
It doesn’t work for intrinsic objects (like window, document etc.) and just returns undefined.