if (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 source object is modified in place. Object.extend also 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.extend all over the place internally: extending built-ins, “mixing in” interfaces, and merging default options with user-defined options. CHAPTER 2 ■ PROTOTYPE BASICS 23 WHY 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 in loop, augment- ing Object.prototype would “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 arguments variable within functions (which con- tains a collection of all the arguments passed to the function). Both types of collections have numeric indices and a length property, 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. $A provides 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. CHAPTER 2 ■ PROTOTYPE BASICS24 The arguments Variable When referenced within a function, arguments holds 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 join method, we first need to convert the arguments variable to an array: function joinArguments() { return $A(arguments).join(', '); } joinArguments('foo', 'bar', 'baz'); //-> "foo, bar, baz" DOM NodeLists A DOM NodeList is 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 a NodeList. 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 CHAPTER 2 ■ PROTOTYPE BASICS 25 // 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; } CHAPTER 2 ■ PROTOTYPE BASICS26 To 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'); CHAPTER 2 ■ PROTOTYPE BASICS 27 The 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 rel attribute. • $$('a[rel~=external]) will select all a elements with the word “external” in the rel attribute. • Querying by adjacency: • $$('ul#menu > li') li') selector> will select all li elements that are direct children of ul#menu. • $$('li.current + li') will select any li sibling that directly follows a li.current in the markup. • $$('li.current ~ li') will select all the following siblings of a li.current element that are li elements themselves. • Negation: • $$('ul#menu li:not(.current)') will select all li elements that don’t have a class name of current. • $$('ul#menu a:not([rel])') will select all a elements that don’t have a rel 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. CHAPTER 2 ■ PROTOTYPE BASICS28 . (person.country == "USA") { Object.extend(data, { socialSecurityNumber: "456 -78 -9012", stateOfResidence: "TX", standardTaxDeduction: true, zipCode: 78 701 }); } Since objects. code both smaller and easier to read. Prototype uses Object.extend all over the place internally: extending built-ins, “mixing in” interfaces, and merging default options with user-defined options. CHAPTER. and a length property, 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. $A