Tải bản đầy đủ (.pdf) (6 trang)

Practical prototype and scipt.aculo.us part 10 pps

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (86.79 KB, 6 trang )

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

×