/* implement _each so we can use Enumerable */_each: functioniterator { $w'foo bar baz'.eachiterator; } }; The difference between a mixin and a class is simple: mixins can’t be instantia
Trang 1Prototype As a Platform
libraries It provides more than just shortcuts—it gives you new ways to structure
your code
In this chapter, we’ll look at some of these tactics and patterns We’ll move beyond
an explanation of what the framework does and into higher-level strategies for solving
problems Some of these are specific code patterns to simplify common tasks; others
make code more modular and adaptable
Using Code Patterns
A script written atop Prototype has a particular style (to call it flair would perhaps be
overindulgent) It’s peppered with the time-saving patterns and terse syntactic shortcuts
that are Prototype’s trademark
I’m calling these code patterns, but please don’t treat them as copy-and-paste
sec-tions of code They’re more like recipes; use them as a guide, but feel free to modify an
ingredient or two as you see fit
Staying DRY with Inheritance and Mixins
Prototype’s class-based inheritance model lets you build a deep inheritance tree
Sub-classes can call all the methods of their parents, even those that have been overridden
Ajax.Request, serves as a superclass for both Ajax.Updaterand Ajax.PeriodicalUpdater
script.aculo.us uses inheritance even more liberally For instance, all core effects inherit
subclassed by the user and customized
Inheritance is a simple solution for code sharing, but it isn’t always the best solution
Sometimes several classes need to share code but don’t lend themselves to any sort of
hierarchical relationship
297
Trang 2That’s where mixins come in Mixins are sets of methods that can be added to any
class, independent of any sort of inheritance A class can have only one parent class— multiple inheritance is not supported—but it can have any number of mixins
con-tains a host of methods that are designed for working with collections of things In Prototype, mixins are simply ordinary objects:
var Enumerable = {
each: function() { /* */ },
findAll: function() { /* */ },
// and so on
};
We can use mixins in two ways:
sim-ply groups of methods that the class will implement The order of the arguments is important; later methods override earlier methods
var Foo = Class.create({
initialize: function() {
console.log("Foo#Base called.");
},
/* implement _each so we can use Enumerable */
_each: function(iterator) {
$w('foo bar baz').each(iterator);
}
});
// mixing in Enumerable after declaration
Foo.addMethods(Enumerable);
// mixing in Enumerable at declaration time
var Bar = Class.create(Enumerable, {
initialize: function() {
console.log("Bar#Base called.");
},
Trang 3/* implement _each so we can use Enumerable */
_each: function(iterator) {
$w('foo bar baz').each(iterator);
}
});
The difference between a mixin and a class is simple: mixins can’t be instantiated
They’re morsels of code that are meaningless on their own but quite powerful when used
in the right context
Example 1: Setting Default Options
Many classes you write will follow the argument pattern of Prototype/script.aculo.us:
the last argument will be an object containing key/value pairs for configuration Most
var Foo = Class.create({
initialize: function(element, options) {
this.element = $(element);
this.options = Object.extend({
duration: 1.0, color: '#fff', text: 'Saving '
}, options || {});
}
});
them with whatever options the user has set We can extract this pattern into one that’s
both easier to grok and friendlier to inherit.
First, let’s move the default options out of the constructor and into a more
constant, but it’ll have capital letters, which will make it look important That’s close
enough
Foo.DEFAULT_OPTIONS = {
duration: 1.0,
color: '#fff',
text: 'Saving '
};
Trang 4Now, let’s create a mixin called Configurable It’ll contain code for working with options
var Configurable = {
setOptions: function(options) {
// clone the defaults to get a fresh copy
this.options = Object.clone(this.constructor.DEFAULT_OPTIONS);
return Object.extend(this.options, options || {});
}
};
To appreciate this code, you’ll need to remember two things First, observe how we clone the default options Since objects are passed by reference, we want to duplicate the object first, or else we’ll end up modifying the original object in place And, as the
var Foo = Class.create({
initialize: function(element, options) {
this.element = $(element);
this.setOptions(options);
}
});
Now, if you’ve been counting lines of code, you’ll have discovered that we wrote about eight lines in order to eliminate about two So far we’re in the hole But let’s take
Configurableone step further by allowing default options to inherit:
var Configurable = {
setOptions: function(options) {
this.options = {};
var constructor = this.constructor;
if (constructor.superclass) {
// build the inheritance chain
var chain = [], klass = constructor;
while (klass = klass.superclass) chain.push(klass);
chain = chain.reverse();
Trang 5for (var i = 0, len = chain.length; i < len; i++)
Object.extend(this.options, klass.DEFAULT_OPTIONS || {});
}
Object.extend(this.options, constructor.DEFAULT_OPTIONS);
return Object.extend(this.options, options || {});
}
};
OK, this one was a big change Let’s walk through it:
magi-cal superclassproperty I told you about? It has a purpose!) If it inherits from
nothing, our task is simple—we extend the default options onto the empty object,
we extend our custom options, and we’re done.
short, we trace the inheritance chain from superclass to superclass until we’ve
col-lected all of the class’s ancestors, in order from nearest to furthest This approach
works no matter how long the inheritance chain is We collect them by pushing
each one into an array as we visit it
the furthest ancestor is at the beginning
empty object but is now accumulating options each time through the loop
default options Now we copy over the default options of the current class, copy
over our custom options, and we’re done
Still with me? Think about how cool this is: default options now inherit I can
GreatGrandfather, and so on If two classes in this chain have different defaults for an
option, the “younger” class wins
You might balk at the amount of code we just wrote, but think of it as a trade-off
Set-ting options the old way is slightly ugly every time we do it SetSet-ting them with the
Configurablemixin is really ugly, but we need to write it only once!
So much of browser-based JavaScript involves capturing this ugliness and hiding it
somewhere you’ll never look Mixins are perfect for this task
Trang 6Example 2: Keeping Track of Instances
Many of the classes we’ve written are meant to envelop one element in the DOM This is the element that we typically pass in as the first argument—the element we call this.element
There should be an easy way to associate an element and the instance of a class that
centers on said element One strategy would be to add a property to the element itself:
Widget.Foo = Class.create({
initialize: function(element, options) {
this.element = $(element);
// store this instance for later
this.element._fooInstance = this;
}
});
This approach works, but at a cost: we’ve just introduced a memory leak into our application Internet Explorer 6 has infamous problems with its JavaScript garbage col-lection (how it reclaims memory from stuff that’s no longer needed): it gets confused
when there are circular references between a DOM node and a JavaScript object The
elementproperty of my instance refers to a DOM node, and that node’s _fooInstance
property refers back to the instance
So in Internet Explorer 6, neither of these objects will be garbage collected—even if the node gets removed from the document, and even if the page is reloaded They’ll con-tinue to reside in memory until the browser is restarted
Memory leaks are insidious and can be very, very hard to sniff out But we can
avoid a great many of them if we follow a simple rule: only primitives should be stored
as custom properties of DOM objects This means strings, numbers, and Booleans are OK; they’re all passed by value, so there’s no chance of a circular reference.
So we’ve settled that But how else can we associate our instance to our element? One approach is to add a property to the class itself What if we create a lookup table for Widget.Foo’s instances onWidget.Fooitself?
Widget.Foo.instances = {};
Then we’ll store the instances as key/value pairs The key can be the element’s ID— it’s unique to the page, after all
Widget.Foo = Class.create({
initialize: function(element, options) {
this.element = $(element);
Widget.Foo[element.id] = this;
}
});