$A: Coercing Collections into Arrays Oftentimes in JavaScript, you’ll have to work with a collection that seems like an array but really isn’t.. The two major culprits are DOM NodeLists
Trang 1if (person.country == "USA") {
Object.extend(data, {
socialSecurityNumber: "456-78-9012",
stateOfResidence: "TX",
standardTaxDeduction: true,
zipCode: 78701
});
}
Since objects are passed by reference, not value, the sourceobject is modified
in place
Object.extendalso solves our typing woes when extending built-ins:
Object.extend(String.prototype, {
strip: function() {
//
},
gsub: function() {
//
},
times: function() {
//
},
toQueryParams: function() {
//
}
});
for (var i in String.prototype)
console.log(i);
//-> "strip", "gsub", "times", "toQueryParams"
That’s one annoyance out of the way This construct cuts down on redundancy,
making code both smaller and easier to read Prototype uses Object.extendall over
the place internally: extending built-ins, “mixing in” interfaces, and merging default
options with user-defined options
Trang 2WHY NOT USE OBJECT.PROTOTYPE.EXTEND?
If we were steadfastly abiding by JavaScript’s object orientation, we’d define Object.prototype extend, so that we could say the following:
var data = { height: "5ft 10in", hair: "brown" };
data.extend({
socialSecurityNumber: "456-78-9012",
stateOfResidence: "TX"
});
This may appear to make things easier for us, but it will make things much harder elsewhere Because properties defined on the prototypes of objects are enumerated in a for inloop, augment-ing Object.prototypewould “break” hashes:
for (var property in data)
console.log(property);
//-> "height", "hair", "socialSecurityNumber", "stateOfResidence", "extend" There are ways around this, but they all involve changing the way we enumerate over objects And we’d be breaking a convention that’s relied upon by many other scripts that could conceivably exist in the same environment as Prototype In the interest of “playing well with others,” nearly all modern JavaScript libraries abide by a gentleman’s agreement not to touch Object.prototype
$A: Coercing Collections into Arrays
Oftentimes in JavaScript, you’ll have to work with a collection that seems like an array but
really isn’t The two major culprits are DOM NodeLists (returned by getElementsByTagName and other DOM methods) and the magic argumentsvariable within functions (which con-tains a collection of all the arguments passed to the function)
Both types of collections have numeric indices and a lengthproperty, just like arrays—but because they don’t inherit from Array, they don’t have the same methods that arrays have For most developers, this discovery is sudden and confusing
$Aprovides a quick way to get a true array from any collection It iterates through the collection, pushes each item into an array, and returns that array
Trang 3The arguments Variable
When referenced within a function, argumentsholds a collection of all the arguments
passed to the function It has numeric indices just like an array:
function printFirstArgument() {
console.log(arguments[0]);
}
printFirstArgument('pancakes');
//-> "pancakes"
It isn’t an array, though, as you’ll learn when you try to use array methods on it.
function joinArguments() {
return arguments.join(', ');
}
joinArguments('foo', 'bar', 'baz');
//-> Error: arguments.join is not a function
To use the joinmethod, we first need to convert the argumentsvariable to an array:
function joinArguments() {
return $A(arguments).join(', ');
}
joinArguments('foo', 'bar', 'baz');
//-> "foo, bar, baz"
DOM NodeLists
A DOM NodeListis the return value of any DOM method that fetches a collection of
elements (most notably getElementsByTagName) Sadly, DOM NodeLists are nearly
use-less They can’t be constructed manually by the user They can’t be made to inherit
from Array And the same cross-browser issues that make it hard to extend HTMLElement
also make it hard to extend NodeList
Any Prototype method that returns a collection of DOM nodes will use an array
But native methods (like getElementsByTagName) and properties (like childNodes) will
return aNodeList Be sure to convert it into an array before you attempt to use array
methods on it
// WRONG:
var items = document.getElementsByTagName('li');
items = paragraphs.slice(1);
//-> Error: items.slice is not a function
Trang 4// RIGHT:
var items = $A(document.getElementsByTagName('li'));
items = items.slice(1);
//-> (returns all list items except the first)
$$: Complex Node Queries
The richness of an HTML document is far beyond the querying capabilities of the basic DOM methods What happens when we need to go beyond tag name queries and fetch elements by class name, attribute, or position in the document?
Cascading Style Sheets (CSS) got this right CSS, for those of you who don’t have design experience, is a declarative language for defining how elements look on a page The structure of a CSS file consists of selectors, each with a certain number of rules
(i.e., “The elements that match this selector should have these style rules.”) A CSS file,
if it were obsessively commented, might look like this:
body { /* the BODY tag */
margin: 0; /* no space outside the BODY */
padding: 0; /* no space inside the BODY */
}
a { /* all A tags (links) */
color: red; /* links are red instead of the default blue */
text-decoration: none; /* links won't be underlined */
}
ul li { /* all LIs inside a UL */
background-color: green;
}
ul#menu { /* the UL with the ID of "menu" */
border: 1px dotted black; /* a dotted, 1-pixel black line around the UL */ }
ul li.current {
/* all LIs with a class name of "current" inside a UL */
background-color: red;
}
Trang 5To put this another way, one side of our problem is already solved: in CSS, there
exists a syntax for describing specific groups of nodes to retrieve Prototype solves the
other side of the problem: writing the code to parse these selectors in JavaScript and
turn them into collections of nodes
The $$function can be used when simple ID or tag name querying is not powerful
enough Given any number of CSS selectors as arguments, $$will search the document
for all nodes that match those selectors
$$('li'); // (all LI elements)
//-> [<li class="current" id="nav_home">, <li id="nav_archives">,
<li id="nav_contact">, <li id="nav_google">]
$$('li.current'); // (all LI elements with a class name of "current")
//-> [<li class="current" id="nav_home">]
$$('#menu a'); // (all A elements within something with an ID of "menu")
//-> [<a href="/">, <a href="/archives">, <a href="/contact">,
<a href="http://www.google.com" rel="external">]
There are two crucial advantages $$has over ordinary DOM methods The first is
brevity: using $$cuts down on keystrokes, even for the simplest of queries
// BEFORE:
var items = document.getElementsByTagName('li');
// AFTER:
var items = $$('li');
As the complexity of your query increases, so does the savings in lines of code $$
can be used to fetch node sets that would take many lines of code to fetch otherwise:
// find all LI children of a UL with a class of "current"
// BEFORE:
var nodes = document.getElementsByTagName('li');
var results = [];
for (var i = 0, node; node = nodes[i]; i++) {
if (node.parentNode.tagName.toUpperCase() == 'UL' &&
node.className.match(/(?:\s*|^)current(?:\s*|$)) {
results.push(node);
}
}
// AFTER:
var results = $$('ul > li.current');
Trang 6The second advantage is something we’ve talked about already: the nodes returned
by $$are already “extended” with Prototype’s node instance methods
If you’re a web designer, you’re likely familiar with CSS, but the power of $$goes far beyond the sorts of selectors you’re likely accustomed to $$supports virtually all of CSS3 syntax, including some types of selectors that you may not have encountered:
• Querying by attribute:
• $('input[type="text"]')will select all text boxes
• $$('a[rel]')will select all anchor tags with a relattribute
• $$('a[rel~=external])will select all aelements with the word “external” in the relattribute
• Querying by adjacency:
• $$('ul#menu > li') li') selector>will select all lielements that are direct children of ul#menu
• $$('li.current + li')will select any lisibling that directly follows a li.currentin the markup
• $$('li.current ~ li')will select all the following siblings of a li.current element that are lielements themselves
• Negation:
• $$('ul#menu li:not(.current)')will select all lielements that don’t have a
class name of current
• $$('ul#menu a:not([rel])')will select all aelements that don’t have arel attribute
These are just some of the complex selectors you can use in $$ For more information
on what’s possible, consult the Prototype API reference online (http://prototypejs.org/ api/) We’ll encounter other complex selectors in some of the code we’ll write later in this book