This is a fine and decent way to loop, but JavaScript is capable of so much more!A language with JavaScript’s expressive power can embrace functional programming concepts to make iterati
Trang 1You can do far more with Prototype than what I’ve just described, but the functions in
this chapter are the ones you’ll use most often And although they solve common
prob-lems, they also form the foundation for a general scripting philosophy: one that espouses
fewer lines of code, separation of content and behavior, and the principle of least
sur-prise Later on, you’ll learn how to use these functions within a set of conventions to
make your DOM scripting experience far more pleasant
C H A P T E R 2 ■ P R OTOT Y P E B A S I C S 29
Trang 2Collections (Or, Never Write a
for Loop Again)
Collections are at the heart of DOM scripting—arrays, hashes, DOM NodeLists, and
various other groups of items Nearly all your scripts will do some form of iteration over
an array So why is iteration so bland in JavaScript?
Prototype sports a robust library for dealing with collections It makes arrays
astoundingly flexible (and invents Hash, a subclass of Object, for key/value pairs), but
can also be integrated into any collections you use in your own scripts
The Traditional for Loop
Amazingly, the first version of JavaScript didn’t even support arrays They were added
soon after, but with only one real enhancement over a vanilla Object—a magic length
property that would count the number of numeric keys in the array For example
var threeStooges = new Array();
threeStooges[0] = "Larry";
threeStooges[1] = "Curly";
threeStooges[2] = "Moe";
console.log(threeStooges.length);
//-> 3
The lengthproperty and the ubiquitous forlooping construct result in a simple,
low-tech way to loop over an array’s values: start at 0and count up to the value of length
for (var i = 0; i < threeStooges.length; i++) {
console.log(threeStooges[i] + ": Nyuk!");
}
31
Trang 3This is a fine and decent way to loop, but JavaScript is capable of so much more!
A language with JavaScript’s expressive power can embrace functional programming
concepts to make iteration smarter
Functional Programming
JavaScript is a multi-paradigm language It can resemble the imperative style of C, the object-oriented style of Java, or the functional style of Lisp To illustrate this, let’s define
a function and see what it can do:
function makeTextRed(element) {
element.style.color = "red";
}
This function expects a DOM element node and does exactly what it says: it turns the node’s enclosed text red To apply this function to an entire collection of nodes, we can use the venerable forloop:
var paragraphs = $$('p');
for (var i = 0; i < elements.length; i++)
makeTextRed(elements[i]);
But let’s look at this from another angle You learned in Chapter 1 that functions in JavaScript are “first-class objects,” meaning that they can be treated like any other data type, like so:
typeof makeTextRed //-> "function"
makeTextRed.constructor; //-> Function
var alias = makeTextRed;
alias == makeTextRed; //-> true
In short, anything that can be done with strings, numbers, or other JavaScript data types can be done with functions
This enables a different approach to iteration: since functions can be passed as arguments to other functions, you can define a function for iterating over an array Again, this is easier to explain with code than with words:
function each(collection, iterator) {
for (var i = 0; i < collection.length; i++)
iterator(collection[i]);
}
C H A P T E R 3 ■ C O L L E C T I O N S ( O R , N E V E R W R I T E A F O R L O O P A G A I N )
32
Trang 4The iteratorargument is a function The eachmethod we just wrote will loop over an
array’s indices and call iteratoron each, passing into it the current item in the array
Now we can iterate thusly:
var paragraphs = $$('p');
each(paragraphs, makeTextRed);
Also remember from Chapter 1 that functions have a literal notation—you don’t have
to name a function before you use it If we won’t use the makeTextRedfunction anywhere
else in the code, then there’s no reason to define it beforehand
each(paragraphs, function(element) {
element.style.color = "red";
});
We can make one more improvement to our code Since each is a method made to
act on arrays, let’s make it an instance method of all arrays:
Array.prototype.each = function(iterator) {
for (var i = 0; i < this.length; i++)
iterator(this[i]);
};
Remember that thisrefers to the execution scope of the function—in this case, it’s
the array itself Now we can write the following:
paragraphs.each(function(element) {
element.style.color = "red";
});
To look at this more broadly, we’ve just abstracted away the implementation details
of iteration Under the hood, we’re calling the same old forloop, but because we’ve built
a layer on top, we’re able to define other functions that involve iterating but do much
more than the preceding eachexample
ABOUT FUNCTION NOTATION
This book uses a common notation to distinguish between static methods and instance methods Static
methods are marked with a dot—for example,Array.fromrefers to the frommethod on the Array
object Instance methods are marked with an octothorpe:Array#eachrefers to the eachmethod on
Array.prototype—that is, a method on an instance of Array
Trang 5Prototype’s Enumerable Object
Prototype defines a handful of functions in an object called Enumerable Anything that is
“enumerable” (anything that can be iterated over) can use these methods
These functions include each(much like the one we defined previously) and many other methods that all hook into eachinternally These methods aren’t specific to
arrays—they can be used on any collection, as long as we tell them how to enumerate
the items therein
Prototype automatically extends Enumerableonto Array At the end of the chapter, you’ll learn how to implement Enumerablein your own classes, but for now we’ll use arrays for all our examples
Using Enumerable#each
Enumerable#eachis the foundation that the rest of Enumerablerelies upon, so let’s take
a closer look at it
I’ve been defaming forloops for several pages now, but they do have one critical advantage over functional iteration: they let you short-circuit the iteration flow by using the keywords break(abort the loop) and continue(skip to the next item in the loop) We need a way to emulate these keywords if we want to match the feature set
of traditional loops
var elements = $$('.menu-item');
// find the element whose text content contains "weblog"
for (var i = 0, element; element = elements[i]; i++) {
if (!element.id) continue;
if (element.innerHTML.include('weblog')) break;
}
Simulating continueis easy enough—an empty return within a function will do the trick:
var elements = $$('.menu-item'), weblogElement;
elements.each( function(element) {
if (!element.id) return;
/* */
});
But a breakequivalent takes a bit of voodoo Prototype makes smart use of excep-tions to pull this off It creates a $breakobject that can be thrown within loops to exit immediately
C H A P T E R 3 ■ C O L L E C T I O N S ( O R , N E V E R W R I T E A F O R L O O P A G A I N )
34
Trang 6var elements = $$('.menu-item'), weblogElement;
elements.each( function(element) {
if (!element.id) return;
if (element.innerHTML.include('weblog')) {
weblogElement = element;
throw $break;
}
});
In this example, we “throw” the $breakobject as though it were an exception It
interrupts the execution of the function and gets “caught” higher in the call stack, at
which point the eachmethod stops iterating and moves on
Now we’re even It’s rare that you’ll need to use $break—most of the use cases for
breaking out of loops are addressed by other Enumerablemethods—but it’s comforting
to know it’s there
Finding Needles in Haystacks: detect, select,
reject, and partition
The code pattern we used in the last section—finding one needle in a haystack—is a
common one, but we can express it more concisely than through an eachloop The
function we pass into eachserves as an item manipulator, but we can also use that
function as a litmus test to let us know whether an item matches our needle The next
four methods do just this
Using Enumerable#detect
Enumerable#detectfinds and returns one item in your collection It takes a function as
an argument (one that returns trueor false) and will return the first item in the
collec-tion that causes the funccollec-tion to returntrue
function isEven(number) {
return number % 2 == 0;
}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].detect(isEven);
//-> 2
If there are no matches,detectwill returnfalse