Brilliant! Now, to grab a class instance from the associated element, we can use its ID: var someInstance = Widget.Foo.instances[someElement.id]; We could stop right here and be happy with ourselves. But let’s consider some edge cases first: What if the element doesn’t have an ID? Then our key will be null. To ensure that the element has an ID, we can use Element#identify. The method returns the element’s ID if it already exists; if not, it assigns the element an arbitrary ID and returns it to us. What if we don’t know the name of the class? In order to move this code into a mixin, we’ll have to remove any explicit references to the class’s name. Luckily, we’ve already got the answer to this one: the instance’s constructor property, which points back to the class itself. What if that element already has an instance and another one gets created? For this case, we’ll assume that only one instance per element is needed. When a new one gets declared, it would be nice if we cleaned up the old one somehow. First, let’s make a mixin called Trackable. It’ll contain the code for keeping track of a class’s instances. Let’s also create a register method, which should be the only one we need for this exercise. It’ll add the instance to the lookup table. var Trackable = { register: function() { } }; Now we’ll solve our problems one by one. First, let’s grab the element’s ID. If the class doesn’t have an element property, we’ll simply return false. (You may choose to throw an exception instead; just make sure you handle this case one way or another.) var Trackable = { register: function() { if (!this.element) return false; var id = this.element.identify(); } }; Next, we’ll use the constructor property to reach the class itself. This way we don’t have to call it by name. We’ll also create the instances property if it doesn’t already exist. CHAPTER 13 ■ PROTOTYPE AS A PLATFORM 303 var Trackable = { register: function() { if (!this.element) return false; var id = this.element.identify(); var c = this.constructor; if (!c.instances) c.instances = {}; c.instances[id] = this; } }; Now we’ll address that last edge case. If a class needs some sort of cleanup before it gets removed, we’ll have to rely on the class itself to tell us how. So let’s adopt a conven- tion: assume the cleanup “instructions” are contained in a method called destroy. This method might remove event listeners, detach some nodes from the DOM, or stop any timers set using setTimeout or setInterval. This method will handle cleanup when we need to replace an instance that’s already in the table. We can check the instance to be replaced to see whether it has a destroy method; if so, we’ll call it before replacing the instance in the lookup table. var Trackable = { register: function() { if (!this.element) return false; var id = this.element.identify(); var c = this.constructor; if (!c.instances) c.instances = {}; if (c.instances[id] && c.instances[id].destroy) c.instances[id].destroy(); c.instances[id] = this; } }; And we’re done. Our new mixin is small but useful. And including it in a class is as simple as passing it into Class.create. The only other thing to remember is to call the register method sometime after assigning the element property. CHAPTER 13 ■ PROTOTYPE AS A PLATFORM304 Widget.Foo = Class.create(Trackable, { initialize: function(element, options) { this.element = $(element); this.register(); this.addObservers(); }, addObservers: function() { // We store references to this bound function so that we can remove them // later on. this.observers = { mouseOver: this.mouseOver.bind(this), mouseOut: this.mouseOut.bind(this); } this.element.observe("mouseover", this.observers.mouseOver); this.element.observe("mouseout", this.observers.mouseOut); }, destroy: function() { this.element.stopObserving("mouseover", this.observers.mouseOver); this.element.stopObserving("mouseout", this.observers.mouseOut); } }); The mixin takes care of the rest. Write it once and it’ll be useful for the remainder of your scripting career. Solving Browser Compatibility Problems: To Sniff or Not to Sniff? So, if some browsers are more ornery than others, how can we tell which is which? The obvious approach would be sniffing—checking the browser’s user-agent string. In JavaScript, this string lives at navigator.userAgent. Looking for telltale text (e.g., “MSIE” for Internet Explorer or “AppleWebKit” for Safari) usually lets us identify the specific browser being used, even down to the version number. CHAPTER 13 ■ PROTOTYPE AS A PLATFORM 305 Browser sniffing is problematic, though—the sort of thing you’d get dirty looks for at web design meetups and tech conferences. Among the biggest problems is that there are more browsers on earth than the average web developer knows about, and when doing browser sniffing, it’s too easy to write code that leaves some poor saps out in the cold. Also troublesome is that browsers have an incentive to imitate one another in their user-agent strings, thereby diluting the value of the information. For years, Opera (which supports a number of Internet Explorer’s proprietary features) masqueraded as Internet Explorer in its user-agent string. Better to do so than to be arbitrarily shut out of a site that would almost certainly work in your browser. Finally, though, the problem with browser sniffing is arguably one of coding philoso- phy: is it the right question to ask? Quite often we need to distinguish between browsers because of their varying levels of support for certain features. The real question, then, is “Do you support feature X?” instead of “Which browser are you?” This debate is oddly complex. It’s important because we need to assess what a user’s browser is capable of. But before we go further, we ought to make a distinction between capabilities and quirks. Capabilities Support Capabilities are things that some browsers support and others don’t. DOM Level 2 Events is a capability; Firefox supports it, but Internet Explorer does not (as of version 7). DOM Level 3 XPath is a capability; Safari 3 supports it, but Safari 2 does not. Other capabilities are supported by all modern browsers, so we take them for granted. All modern browsers support document.getElementById (part of DOM Level 1 Core), but once upon a time this wasn’t true. Nowadays only the most paranoid of DOM scripters tests for this function before using it. Capabilities are not specific to browsers. They’re nearly always supported by specifi- cations (from the W3C or the WHATWG, for instance) and will presumably be supported by all browsers eventually. To write code that relies on capabilities, then, you ought to be singularly concerned with the features a browser claims to support, not the browser’s name. Since functions are objects in JavaScript, we can test for their presence in a conditional: // document.evaluate is an XPath function if (document.evaluate) { // fetch something via XPath } else { // fetch something the slower, more compatible way } CHAPTER 13 ■ PROTOTYPE AS A PLATFORM306 Here we’re testing whether the document.evaluate function exists. If so, the condi- tional evaluates to true, and we reap the benefits of lightning-fast DOM traversal. If not, the conditional evaluates to false, and we reluctantly traverse the DOM using slower methods. Testing for capabilities makes our code future-proof. If a future version of Internet Explorer supports XPath, we don’t have to change our detection code, because we’re test- ing for the feature, not the browser. Therefore, it’s a far better idea to test for capabilities than to infer them based on the name of a browser. It’s not the meaningless distinction of a pedant. Code written with a capability-based mindset will be hardier and of a higher quality. Quirks and Other Non-Features There’s a dark side, though. JavaScript developers also have to deal with quirks. A quirk is a polite term for a bug—an unintended deviation from the standard behavior. Internet Explorer’s aforementioned memory leaks are a quirk. Internet Explorer 6, a browser that many web users still run today, has been around since 2001, enough time to find all sorts of bizarre bugs in rendering and scripting. To be clear, though, all browsers have quirks (some more than others, to be sure). But quirks are different from capabilities. They’re nearly always specific to one browser; two different browsers won’t have the same bugs. I wish I could present some sort of uniform strategy for dealing with quirks, but they’re too varied to gather in one place. Let’s look at a few examples. Quirk Example 1: Internet Explorer and Comment Nodes The DOM specs treat HTML/XML comment nodes (<! like these >) differently from, say, element nodes. Comments have their own node type, just like text nodes or attribute nodes. In Internet Explorer, comment nodes are treated as element nodes with a tag name of !. They report a nodeType of 1, just like an element would. Calling document.getElementsByTagName('*') will, alongside the element nodes you’d expect, return any comments you’ve declared in the body. This is incorrect, to put it mildly. More vividly, it’s the sort of bug that would make a developer embed her keyboard into her own forehead if she weren’t aware of it and had encountered it on her own. So how do we work around quirks? It depends. One strategy is to treat them just like capabilities—see if you can reproduce the bug, and then set some sort of flag if you can: CHAPTER 13 ■ PROTOTYPE AS A PLATFORM 307 var thinksCommentsAreElements = false; if (document.createElement('!').nodeType === 1) { thinksCommentsAreElements = true; } Once you’ve set this flag, you can use it inside your own functions to give extra logic to Internet Explorer. This approach has the same upsides of capability detection: instead of blindly assuming that all versions of Internet Explorer exhibit this quirk, we find out for sure. If Internet Explorer 8 fixes this bug, it avoids the workaround altogether. Quirk Example 2: Firefox and Ajax Versions of Firefox prior to 1.5 exhibit a behavior that can throw a wrench into the Ajax gears. An affected browser will, in an Ajax context, sometimes give the wrong Content-Length of an HTTP POST body—thereby flummoxing servers that find a line feed after the request was supposed to have ended. The workaround is simple enough: force a Connection: close header so that the server knows not to keep listening (in other words, tell the server that the line feed can be ignored). But figuring out when the workaround is needed turns out to be very, very ugly. Here are a few lines from the Prototype source code. We’ve dealt with this bug so that you won’t have to, but here’s the workaround: /* Force "Connection: close" for older Mozilla browsers to work * around a bug where XMLHttpRequest sends an incorrect * Content-length header. See Mozilla Bugzilla #246651. */ if (this.transport.overrideMimeType && (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) headers['Connection'] = 'close'; I hate to bring this code out for exhibition. It’s like bringing your angst-ridden teenage poetry to a first date. But unlike the melodramatic sonnets you wrote after your junior prom, this code is quite purposeful. We can’t treat this quirk like a capability because we can’t test for it. To test for it, we’d need to send out an Ajax request while the script initializes. Likewise, we can’t apply the workaround to all browsers, because we’d interfere with use cases where the connection should not be closed (like HTTP keep-alive connections). CHAPTER 13 ■ PROTOTYPE AS A PLATFORM308 . on. this.observers = { mouseOver: this.mouseOver.bind(this), mouseOut: this.mouseOut.bind(this); } this.element.observe("mouseover", this.observers.mouseOver); this.element.observe("mouseout",. this.observers.mouseOut); }, destroy: function() { this.element.stopObserving("mouseover", this.observers.mouseOver); this.element.stopObserving("mouseout", this.observers.mouseOut); } }); The. Internet Explorer or “AppleWebKit” for Safari) usually lets us identify the specific browser being used, even down to the version number. CHAPTER 13 ■ PROTOTYPE AS A PLATFORM 305 Browser sniffing