var obj = {}; obj.constructor; //-> Object obj = { name: "Statue of Liberty", constructor: "Frédéric Bartholdi" }; obj.constructor; //-> "Frédéric Bartholdi" In this example, a built-in property (constructor) that has special meaning in JavaScript is being shadowed by a property of the same name that we assign on the object instance. Similar collisions can occur with toString, valueOf, and other built-in properties. The safety of any arbitrary key cannot be guaranteed. These might seem like edge cases, but key safety is especially important when you’re building a hash in which the key names depend on user input, or on some other means that isn’t planned beforehand by the developer. The Object.prototype Problem As we briefly covered in Chapter 2, JavaScript suffers from a flaw caused by two of its fea- tures stepping on one another. In theory, we can define properties on Object.prototype and have them propagate to every instance of Object. Unfortunately, when properties are enumerated in a for in loop, anything that’s been defined on Object.prototype will get picked up. Object.prototype.each = function(iterator) { for (var i in this) iterator(i, this[i]); }; var obj = { name: "Statue of Liberty", constructor: "Frédéric Bartholdi" }; obj.each(console.log); // (pass the key and value as arguments to console.log) >>> "name" "Statue of Liberty" >>> "constructor" "Frédéric Bartholdi" >>> "each" "function(iterator) { for (var i in this) iterator(i, this[i]); }" CHAPTER 3 ■ COLLECTIONS (OR, NEVER WRITE A FOR LOOP AGAIN)42 Regrettably, there’s no way to suppress this behavior. We could get around it by avoiding ordinary for in loops altogether, wrapping code around them that ensures we enumerate only properties that exist on the instance, but then we’ve only solved the problem for our own scripts. Web pages often pull in scripts from various sources, some of which may be unaware of each other’s existence. We can’t expect all these scripts to boil the ocean just to make our lives a little easier. The Solution There’s a way around all this, although it may not be ideal: creating a new “class” for creating true hashes. That way we can define instance methods on its prototype with- out encroaching on Object.prototype. It also means we can define methods for getting and setting keys—internally they can be stored in a way that won’t collide with built-in properties. Prototype’s Hash object is meant for key/value pairs. It is designed to give the syntac- tic convenience of Enumerable methods without encroaching on Object.prototype. To create a hash, use new Hash or the shorthand $H: var airportCodes = new Hash(); To set and retrieve keys from the hash, use Hash#set and Hash#get, respectively: airportCodes.set('AUS', 'Austin-Bergstrom Int'l'); airportCodes.set('HOU', 'Houston/Hobby'); airportCodes.get('AUS'); //-> "Austin-Bergstrom Int'l" Ick! Do we really have to set keys individually like that? Is this worth the trade-off? Luckily, we don’t have to do it this way. We can pass an object into the Hash construc- tor to get a hash with the key/value pairs of the object. Combine this with the $H shortcut, and we’ve got a syntax that’s almost as terse as the one we started with: var airportCodes = $H({ AUS: "Austin-Bergstrom Int'l", HOU: "Houston/Hobby", IAH: "Houston/Intercontinental", DAL: "Dallas/Love Field", DFW: "Dallas/Fort Worth" }); You can also add properties to a hash en masse at any time using Hash#update: CHAPTER 3 ■ COLLECTIONS (OR, NEVER WRITE A FOR LOOP AGAIN) 43 var airportCodes = new Hash(); airportCodes.update({ AUS: "Austin-Bergstrom Int'l", HOU: "Houston/Hobby", IAH: "Houston/Intercontinental", DAL: "Dallas/Love Field", DFW: "Dallas/Fort Worth" }); This code gives the same result as the preceding example. You can get a hash’s keys or values returned as an array with Hash#keys and Hash#values, respectively: airportCodes.keys(); //-> ["AUS", "HOU", "IAH", "DAL", "DFW"] airportCodes.values(); //-> ["Austin-Bergstrom Int'l", "Houston/Hobby", "Houston/Intercontinental", //-> "Dallas/Love Field", "Dallas/Fort Worth"] Finally, you can convert a hash back to a plain Object with Hash#toObject: airportCodes.toObject(); //-> { //-> AUS: "Austin-Bergstrom Int'l", //-> HOU: "Houston/Hobby", //-> IAH: "Houston/Intercontinental", //-> DAL: "Dallas/Love Field", //-> DFW: "Dallas/Fort Worth" //-> } Enumerable Methods on Hashes Enumerable methods on hashes work almost identically to their array counterparts. But since there are two parts of each item—the key and the value—the object passed into the iterator function works a bit differently: airportCodes.each( function(pair) { console.log(pair.key + ' is the airport code for ' + pair.value + '.'); }); CHAPTER 3 ■ COLLECTIONS (OR, NEVER WRITE A FOR LOOP AGAIN)44 >>> AUS is the airport code for Austin-Bergstrom Int'l. >>> HOU is the airport code for Houston/Hobby. >>> IAH is the airport code for Houston/Intercontinental. >>> DAL is the airport code for Dallas/Love Field. >>> DFW is the airport code for Dallas/Fort Worth. The pair object in the preceding example contains two special properties, key and value, which contain the respective parts of each hash item. If you prefer, you can also refer to the key as pair[0] and the value as pair[1]. Keep in mind that certain Enumerable methods are designed to return arrays, regard- less of the original type of the collection. Enumerable#map is one of these methods: airportCodes.map( function(pair) { return pair.key.toLowerCase(); }); //-> ["aus", "hou", "iah", "dal", "dfw"] In general, these methods work the way you’d expect them to. Be sure to consult the Prototype API documentation if you get confused. ObjectRange Prototype’s ObjectRange class is an abstract interface for declaring a starting value, an ending value, and all points in between. In practice, however, you’ll be using it almost exclusively with numbers. To create a range, use new ObjectRange or the shorthand $R. var passingGrade = $R(70, 100); var teenageYears = $R(13, 19); var originalColonies = $R(1, 13); A range can take three arguments. The first two are the start point and endpoint of the range. The third argument, which is optional, is a Boolean that tells the range whether to exclude the end value. var inclusiveRange = $R(1, 10); // will stop at 10 var exclusiveRange = $R(1, 10, true); // will stop at 9 Ranges are most useful when you need to determine whether a given value falls within some arbitrary boundaries. The include instance method will tell you whether your value is included in the range. CHAPTER 3 ■ COLLECTIONS (OR, NEVER WRITE A FOR LOOP AGAIN) 45 if (passingGrade.include(studentGrade)) advanceStudentToNextGrade(); But ranges can also use any method in Enumerable. We can take advantage of this to simplify our example code from earlier in the chapter. function isEven(num) { return num % 2 == 0; } var oneToTen = $R(1, 10); oneToTen.select(isEven); //-> [2, 4, 6, 8, 10] oneToTen.reject(isEven); //-> [1, 3, 5, 7, 9] var firstTenSquares = oneToTen.map ( function(num) { return num * num; } ); //-> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] Turning Collections into Arrays Enumerable also boasts a generic toArray method that will turn any collection into an array. Obviously, this isn’t very useful for arrays themselves, but it’s a convenience when working with hashes or ranges. $H({ foo: 'bar', baz: 'thud' }).toArray(); //-> [ ['foo', 'bar'], ['baz', 'thud'] ] $R(1, 10, true).toArray(); //-> [1, 2, 3, 4, 5, 6, 7, 8, 9] Keep in mind that using $A, the array-coercion shortcut function, will produce the same result. If an object has a toArray method, $A will use it. Using Enumerable in Your Own Collections The fun is not confined to arrays, hashes, and ranges. Enumerable can be “mixed into” any class—all it needs to know is how to enumerate your collections. Let’s take a look at the source code for Enumerable#each: // Excerpt from the Prototype source code each: function(iterator) { var index = 0; CHAPTER 3 ■ COLLECTIONS (OR, NEVER WRITE A FOR LOOP AGAIN)46 try { this._each(function(value) { iterator(value, index++); }); } catch (e) { if (e != $break) throw e; } return this; } The nonhighlighted portion manages the boring stuff: keeping track of the index value and catching our handy $break exception. The actual enumeration is delegated to a method called _each. This is where the magic happens. Enumerable needs the _each method to tell it how to enumerate. For example, Array.prototype._each looks like this: // Excerpt from the Prototype source code each: function(iterator) { for (var i = 0, length = this.length; i < length; i++) iterator(this[i]); } So, we haven’t gotten rid of the for loop entirely—we’ve just stashed it away in a function you’ll never call directly. Here’s a horribly contrived example to illustrate all this. Let’s say we want a specific kind of array that will enumerate only its even indices, skipping over the odd ones. Writ- ing the constructor for this class is easy enough: var EvenArray = function(array) { this.array = array; }; We can feed it any ordinary array by calling this constructor with an existing array: var even = new EvenArray(["zero", "one", "two", "three", "four", "five"]); Now let’s define an _each method on our class’s prototype: EvenArray.prototype._each = function(iterator) { for (var i = 0; i < this.array.length; i+=2) iterator(this.array[i]); }; CHAPTER 3 ■ COLLECTIONS (OR, NEVER WRITE A FOR LOOP AGAIN) 47 . Hash or the shorthand $H: var airportCodes = new Hash(); To set and retrieve keys from the hash, use Hash#set and Hash#get, respectively: airportCodes.set('AUS', 'Austin-Bergstrom. shortcut, and we’ve got a syntax that’s almost as terse as the one we started with: var airportCodes = $H({ AUS: "Austin-Bergstrom Int'l", HOU: "Houston/Hobby", IAH: "Houston/Intercontinental", DAL:. depend on user input, or on some other means that isn’t planned beforehand by the developer. The Object .prototype Problem As we briefly covered in Chapter 2, JavaScript suffers from a flaw caused