Each class object comes with a method, addMethods, that lets us add instance meth-ods to the class later on: Player.addMethods{ makeActive: function { this.isActive = true; console.logth
Trang 1>> "Andrew Dupont throws for 23yds."
andrew.passingYards; //-> 23
andrew.throwTouchdown(39);
>> "Andrew Dupont throws for 39yds."
>> "TOUCHDOWN!"
andrew.passingYards; //-> 62
andrew.points; //-> 6
Everything works as expected So let’s try another position A wide receiver plays on
offense and catches passes thrown by the quarterback
var WideReceiver = Class.create(Player, {
initialize: function(firstName, lastName) {
// call Player's initialize method
$super(firstName, lastName);
// define properties for receivers
this.receivingYards = 0;
},
catchPass: function(yards) {
console.log(this + ' catches a pass for ' + yards + 'yds');
this.receivingYards += yards;
},
catchTouchdown: function(yards) {
this.catchPass(yards);
console.log('TOUCHDOWN!');
this.scorePoints(6);
}
});
Notice again that we’re not writing copy-and-paste code Our WideReceiverclass
defines only those methods and properties that are unique to wide receivers, deferring to
the Playerclass for everything else
Trang 2Most OOP-like languages treat classes as static—once they’re defined, they’re immutable.
In JavaScript, however, nothing is immutable, and it would be silly to pretend otherwise Instead, Prototype borrows from Ruby once again
In Ruby, all classes are mutable and can be changed at any point This practice is referred to as “monkeypatching” by those who deride it; I’ll refer to it that way simply
because I like words that contain monkey But there’s no negative connotation for me.
Each class object comes with a method, addMethods, that lets us add instance meth-ods to the class later on:
Player.addMethods({
makeActive: function() {
this.isActive = true;
console.log(this + " will be a starter for Sunday's game.");
},
makeReserve: function() {
this.isActive = false;
console.log(this + " will spend Sunday on the bench.");
}
});
So now we’ve got two new methods on the Playerclass for managing team lineups But these methods also propagate to Player’s two subclasses:Quarterbackand
WideReceiver Now we can use makeActiveand makeReserveon all instances of these
classes—even the instances we’ve already created Remember the narcissistic instance
ofQuarterbackI created?
andrew.makeReserve();
>> "Andrew Dupont will spend Sunday on the bench."
andrew.isActive; //-> false
Don’t take this freedom as license to code in a style that is stupid and pointless Most
of the time you won’t need to monkeypatch your own code But Class#addMethodsis quite useful when dealing with code that isn’t yours—a script.aculo.us class, for example, or another class defined by a Prototype add-on
Usage: DOM Behavior Pattern
Prototype’s advanced OOP model is the perfect tonic for the headache of managing a
complex behavior layer For lack of a better term, I’m going to refer to this as the behavior
Trang 3pattern—a class describes some abstract behaviors, and instances of that class apply the
behavior to individual elements
I’ll rephrase that in plain English In Chapter 6, we wrote some JavaScript to manage
computing totals in a data table: given a table of Texas cities alongside their populations,
our code added together all the population numbers, and then inserted the total in a new
row at the bottom of the table
The function we wrote got the job done, but was written with specifics in mind In
the interest of reusing code, let’s refactor this function We’ll make the logic more generic
and place it into a class that follows the behavior pattern
function computeTotalForTable(table) {
// Accept a DOM node or a string
table = $(table);
// Grab all the cells with population numbers in them
var populationCells = table.select('td.number');
// Add the rows together
// (Remember the Enumerable methods?)
var totalPopulation = populationCells.inject(0, function(memo, cell) {
var total = cell.innerHTML;
// To add, we'll need to convert the string to a number
return memo + Number(total);
});
// We've got the total, so let's build a row to put it in
var tr = new Element('tr', { 'class': 'total' });
tr.insert( new Element('th').update('Total') );
tr.insert( new Element('td',
{ 'class': 'number' } ).update(totalPopulation) );
// Insert a cell for the airport code, but leave it empty
tr.insert( new Element('td', { 'class': 'code' }) );
table.down('tbody').insert(tr);
}
What can we improve upon? How can we make this code more generic and versatile?
Trang 4• We should not make assumptions about the layout of the table, nor the order of its columns, nor the way numeric cells are marked The function assumes it ought to add all cells with a class of number, but what if that selector is too simplistic for what
we need?
• To display the total, we build a whole table row in JavaScript, complete with cells, and then insert it at the bottom of the table This approach isn’t very DRY What happens when we add a column to this table in the HTML? There’s a good chance we’ll forget to make the same change in the JavaScript
Above all, we should rewrite this code to be more lightweight Simple things are reusable; complex things are often context specific We can add more complexity later
if need be, but the best foundation is a simple one
Refactoring
What are the simplest possible solutions to the preceding issues?
• We should be able to specify a CSS selector that describes which elements (within the context of a certain container) we want totaled It can have a default (like number) for convenience
• Instead of building our own DOM structure to place the total into, let’s take our cue
from the HTML In other words, the class should accept an existing element on the
page for displaying the total This limits the responsibility of the class, simplifying the code
So let’s write a class called Totaler, starting off with the bare skeleton:
var Totaler = Class.create({
initialize: function() {
}
});
How many arguments should it take? We need at least two things: the context
ele-ment and the “total container” eleele-ment Any other parameters can fall back to intelligent defaults, so we’ll place them in an optionsargument at the end
Trang 5var Totaler = Class.create({
initialize: function(element, totalElement, options) {
this.element = $(element);
this.totalElement = $(totalElement);
}
});
So what are these “options” I speak of? First of all, we should be able to tell Totaler
which elements to pull numbers from So one of them we’ll call selector, and by default
it will have a value of ".number"
var Totaler = Class.create({
initialize: function(element, totalElement, options) {
this.element = $(element);
this.totalElement = $(totalElement);
this.options = {
selector: ".number"
};
}
});
Now we’ve got a default value for selector, but we also want the user to be able to
override this So let’s copy the optionsargument over this.options, letting all
user-specified options trump the defaults:
var Totaler = Class.create({
initialize: function(element, totalElement, options) {
this.element = $(element);
this.totalElement = $(totalElement);
this.options = Object.extend({
selector: ".number"
}, options || {});
}
});
We type options || {}because the user is allowed to omit the optionsargument
entirely: it’s akin to saying, “Extend this.optionswith optionsor, failing that, an empty
object.”
Remember that thisrefers to the class instance itself So we’ve defined three
proper-ties on the instance, corresponding to the three arguments in our constructor These
properties will be attached to each instance of the Totalerclass as it gets instantiated But
to do the actual adding, we’ll write another method:
Trang 6var Totaler = Class.create({
initialize: function(element, totalElement, options) {
this.element = $(element);
this.totalElement = $(totalElement);
this.options = Object.extend({
selector: ".number"
}, options || {});
},
updateTotal: function() {
}
});
Totaler#updateTotalwill select the proper elements, extract a number out of each, and then add them all together, much the same way as before It needn’t take any argu-ments; all the information it needs is already stored within the class
First, it selects the elements by calling Element#selectin the context of the container element
var Totaler = Class.create({
initialize: function(element, totalElement, options) {
this.element = $(element);
this.totalElement = $(totalElement);
this.options = Object.extend({
selector: ".number"
}, options || {});
},
updateTotal: function() {
var numberElements = this.element.select(this.options.selector);
}
});
Totaler#updateTotaluses the selector we assigned in the constructor; anything set as
a property of thiscan be read both inside and outside the class It selects all elements
As before, we use Enumerable#injectto add the numbers together: