Maybe its just me, but what I want from a JavaScript library seems to be diverging from what Prototype provides. What I want, in order of importance, is:
- A cross-browser API that hides some of the major differences between Internet Explorer and standards compliant browsers
- Unobtrusive
- As small as possible, and if that’s not possible, then at least modular
- A selector API
Where Prototype really falls down is on points two and three – its very obtrusive, getting larger by the day and isn’t modular.
Accepting Something for What It Is
Prototype’s greatest sin is its disdain for JavaScript.You can see this disdain shine through in a number of ways.
First, Prototype originated as part of Rails, which provides helpers that use Ruby code to generate JavaScript. If programs could talk, Rails would be saying “Let me take care of this for you since you certainly don’t want to dirty your hands with JavaScript.”
Second, Prototype wastes over 200 lines of code (about 5%) duplicating Ruby’s Enumerable API in JavaScript, for no obvious reason except the developers prefer Ruby’s way of doing things. The problem is that Ruby’s Enumerable API is based on one of the core features of Ruby – its elegant use of anonymous functions (called blocks) to apply snippets of code to a sequence of items. JavaScript has first-class anonymous functions, but it doesn’t have the language support for using them as iterators. As a result, Prototype’s JavaScript code doesn’t look natural because it is working outside the design strengths of JavaScript. And more importantly, it forces Prototype into using exceptions as a iteration signaling method, which is a nasty hack.
For example, let’s look at the any
method. In Ruby, any
? returns true if an item in a list matches some criteria. Thus to find if any number in an array is odd you would write this:
[2, 4, 6, 8, 11].any? do |value| value.even? # even? is from Rails, not Ruby end
In my view, porting any
to JavaScript is of dubious value at best. But let’s look at the contortions that Prototype has to go through to do it:
any: function(iterator, context) { iterator = iterator ? iterator.bind(context) : Prototype.K; var result = false; this.each(function(value, index) { if (result = !!iterator(value, index)) throw $break; }); return result; } each: function(iterator, context) { var index = 0; iterator = iterator.bind(context); try { this._each(function(value) { iterator(value, index++); }); } catch (e) { if (e != $break) throw e; } return this; } _each: function(iterator) { for (var i = 0, length = this.length; i < length; i++) iterator(this[i]); }
The any
method calls each
with calls _each
which then calls your method. And since JavaScript doesn’t support returning values from an anonymous function used as an iterator (there is no yield keyword like in Ruby), the any
method is forced to throw an exception (see $break) to signal that an element has been found. That might seem like a small offense until you are trying debug JavaScript code using Venkman and keep interrupted by meaningless exceptions (which happens if you’ve asked Venkman to stop at all errors and exceptions).
More examples of trying to make JavaScript more like Ruby abound:
- The addition of a Class object that introduces an initialize function, instead of just accepting JavaScript’s combined constructor/initalizer idiom
- A number of useless additions to the String class (methods like succ, times, etc) – 100 plus lines of code
- A number of useless additions to the Array class (methods like succ, times, etc) – a bit less than 100 lines of code
The end result is that over 10% of Prototype is wasted trying to add Ruby like-features to JavaScript that don’t fit well, simply because the Prototype designers prefer Ruby’s idioms over JavaScript’s idioms. The obvious problem is that Prototype is a JavaScript library, not a Ruby library.
Lay Off My Prototypes
Prototype also fails miserably on the unobtrusive test. In its first version, Prototype added methods to JavaScript’s Object prototype – which is a big no-no. Not learning from its past mistakes, the latest version of Prototype has this gem:
(function() { var element = this.Element; this.Element = function(tagName, attributes) { attributes = attributes || { }; tagName = tagName.toLowerCase(); var cache = Element.cache; if (Prototype.Browser.IE && attributes.name) { tagName = '<' + tagName + ' name="' + attributes.name + '">'; delete attributes.name; return Element.writeAttribute(document.createElement(tagName), attributes); } if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); }; Object.extend(this.Element, element || { }); }).call(window);
Take a good, long look at this method. It replaces a browser’s built in Element object, which is used to represent elements in a DOM tree, with an Element function. Replacing a core browser object is nuts. Especially for the ridiculously small payoff. Instead of writing this:
var element = document.createElement('foo') element.id = 7
This change lets you write this:
var element = new Element('foo', {id: 7})
And how many times does Prototype use this function? A measly 4 times! And to add insult to injury, the code as written is broken because it breaks the prototype chain. The last line in the function should be:
this.Element.prototype = element.prototype
Without this line, any custom extensions you’ve made to the Element object are lost. Trust me, it took a good long time to debug why our code no longer worked.
Time for a Diet
Finally, Prototype is getting bigger with every release. Version 1.5 weighs in at 3,396 lines of code while version 1.6 is 4,307 lines, a 27% increase. I’m sure the additional code is useful, but I’m also sure there are great swaths of Prototype that I don’t need. Unfortunately, Prototype doesn’t provide a mechanism to package up only the parts of it you want. When the library was smaller, that was a reasonable decision. But as Prototype continues to grow, there will come a point where its benefits are outweighed by its weight (and for me I’ve passed that point).
So What Next
The last few years have been JavaScript’s golden years, marked by an amazing outpouring of experimentation and creativity that has led to a number of great JavaScript libraries. A huge benefit of this work is revealing the pain points, beyond cross-browser compatibility issues, of working with JavaScript. These issues include the lack of a Selector API, better iterators, better chaining of DOM methods, wordy method names (getElementById), etc.
Of course each library takes its own approach to solving these problems, and with that comes a downside – lockin. For large JavaScript projects, switching between libraries is a boring, tedious, time-consuming undertaking. Which is the reason we’ve remained with Prototype for as long as we have and will continue to do so for a bit longer while we plan our migration to a new library.