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... Amon
Trang 1Brilliant! 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 constructorproperty, 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 registermethod, 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 elementproperty, 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 constructorproperty to reach the class itself This way we don’t
have to call it by name We’ll also create the instancesproperty if it doesn’t already exist
Trang 2var 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 setTimeoutor 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 registermethod sometime after assigning the elementproperty
Trang 3Widget.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
Trang 4Browser 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
}
Trang 5Here we’re testing whether the document.evaluatefunction 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 nodeTypeof 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:
Trang 6var 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-Lengthof 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: closeheader 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)