Besides, serializing object arguments using tddjs.uid, although simple and fast, would cause the method to possibly assign new properties to arguments.. In the next chapter we will take
Trang 1Listing 6.26 Memoizing the Fibonacci sequence in a closure
var fibonacci = (function () { var cache = {};
function fibonacci(x) {
if (x < 2) { return 1;
}
if (!cache[x]) { cache[x] = fibonacci(x - 1) + fibonacci(x - 2);
} return cache[x];
} return fibonacci;
}());
This alternative version of fibonacci runs many orders of magnitude faster than the original one, and by extension is capable of calculating more numbers in the sequence However, mixing computation with caching logic is a bit ugly Again,
we will add a function to Function.prototype to help separate concerns
The memoize method in Listing 6.27 is capable of wrapping a method, adding memoization without cluttering the calculation logic
Listing 6.27 A general purpose memoize method
if (!Function.prototype.memoize) { Function.prototype.memoize = function () { var cache = {};
var func = this;
return function (x) {
if (!(x in cache)) { cache[x] = func.call(this, x);
} return cache[x];
};
};
}
Trang 2This method offers a clean way to memoize functions, as seen in Listing 6.28
Listing 6.28 Memoizing the fibonacci function
TestCase("FibonacciTest", {
"test calculate high fib value with memoization":
function () { var fibonacciFast = fibonacci.memoize();
assertEquals(1346269, fibonacciFast(30));
} });
The memoize method offers a clean solution but unfortunately only deals with functions that take a single argument Limiting its use further is the fact that it
blindly coerces all arguments to strings, by nature of property assignment, which
will be discussed in detail in Chapter 7, Objects and Prototypal Inheritance.
To improve the memoizer, we would need to serialize all arguments to use as keys One way to do this, which is only slightly more complex than what we already
have, is to simply join the arguments, as Listing 6.29 does
Listing 6.29 A slightly better memoize method
if (!Function.prototype.memoize) {
Function.prototype.memoize = function () { var cache = {};
var func = this;
var join = Array.prototype.join;
return function () { var key = join.call(arguments);
if (!(key in cache)) { cache[key] = func.apply(this, arguments);
} return cache[key];
};
};
}
This version will not perform as well as the previous incarnation because it both calls join and uses apply rather than call, because we no longer can assume
the number of arguments Also, this version will coerce all arguments to strings
as before, meaning it cannot differentiate between, e.g., "12" and 12 passed as
Trang 3arguments Finally, because the cache key is generated by joining the parameters with a comma, string arguments that contain commas can cause the wrong value to
be loaded, i.e., (1, "b") would generate the same cache key as ("1,b")
It is possible to implement a proper serializer that can embed type information about arguments, and possibly use tddjs.uid to serialize object and function arguments, but doing so would impact the performance of memoize in a noticeable way such that it would only help out in cases that could presumably be better optimized in other ways Besides, serializing object arguments using tddjs.uid, although simple and fast, would cause the method to possibly assign new properties
to arguments That would be unexpected in most cases and should at the very least
be properly documented
6.5 Summary
In this chapter we have worked through a handful of practical function examples with a special focus on closures With an understanding of the scope chain from
Chapter 5, Functions, we have seen how inner functions can keep private state in
free variables Through examples we have seen how to make use of the scope and state offered by closures to solve a range of problems in an elegant way
Some of the functions developed in this chapter will make appearances in upcoming chapters as we build on top of them and add more useful interfaces to the tddjs object Throughout the book we will also meet plenty more examples
of using closures
In the next chapter we will take a look at JavaScript’s objects and gain a bet-ter understanding of how property access and prototypal inheritance work, how closures can help in object oriented programming in JavaScript, as well as explore different ways to create objects and share behavior between them
Trang 4ptg
Trang 57
Objects and Prototypal
Inheritance
JavaScript is an object oriented programming language However, unlike most other object oriented languages, JavaScript does not have classes Instead, JavaScript
offers prototypes and prototype-based inheritance in which objects inherit from other
objects Additionally, the language offers constructors—functions that create ob-jects, a fact that often confuses programmers and hides its nature In this chapter we’ll investigate how JavaScript objects and properties work We’ll also study the prototype chain as well as inheritance, working through several examples in a test-driven manner
7.1 Objects and Properties
JavaScript has object literals, i.e., objects can be typed directly into a program using
specific syntax, much like string ("a string literal") and number literals (42) can be typed directly in a program in most languages Listing 7.1 shows an example of an object literal
Listing 7.1 An object literal
var car = { model: { year: "1998", make: "Ford", model: "Mondeo"
Trang 6}, color: "Red", seats: 5, doors: 5, accessories: ["Air condition", "Electric Windows"], drive: function () {
console.log("Vroooom!");
} };
Incidentally, Listing 7.1 shows a few other literals available in JavaScript as well, most notably the array literal ([] as opposed to new Array())
ECMA-262 defines a JavaScript object as an unordered collection of properties
Properties consist of a name, a value, and a set of attributes Property names are
either string literals, number literals, or identifiers Properties may take any value,
i.e., primitives (strings, numbers, booleans, null or undefined) and objects,
including functions When properties have function objects assigned to them, we
usually refer to them as methods ECMA-262 also defines a set of internal
proper-ties and methods that are not part of the language, but are used internally by the
implementation The specification encloses names of these internal properties and
methods in double brackets, i.e., [[Prototype]] I will use this notation as well
7.1.1 Property Access
JavaScript properties can be accessed in one of two ways—using dot notation,
car.model.year, or using a style commonly associated with dictionaries or
hashes, car["model"]["year"] The square bracket notation offers a great
deal of flexibility when looking up properties It can take any string or expression
that returns a string This means that you can dynamically figure out the property
name at run-time and look it up on an object directly using the square brackets
Another benefit of the square bracket notation is that you can access properties
whose name contain characters not allowed in identifiers such as white space You
can mix dot and bracket notation at will, making it very easy to dynamically look
up properties on an object
As you might remember, we used property names containing spaces to make
our test case names more human-readable in Chapter 3, Tools of the Trade, as seen in
Listing 7.2
Trang 7Listing 7.2 A property name with spaces
var testMethods = {
"test dots and brackets should behave identically":
function () { var value = "value";
var obj = { prop: value };
assertEquals(obj.prop, obj["prop"]);
} };
// Grab the test var name = "test dots and brackets should behave identically";
var testMethod = testMethods[name];
// Mix dot and bracket notation to get number of expected // arguments for the test method
var argc = testMethods[name].length;
Here we get a test method (i.e., a property) from our object using the square bracket notation, because the name of the property we are interested in contains characters that are illegal in identifiers
It is possible to get and set properties on an object using other values than string literals, number literals, or identifiers When you do so, the object will be converted to a string by its toString method if it exists (and returns a string), or its valueOf method Beware that these methods may be implementation-specific (e.g., for host objects1), and for generic objects the toString method will return
"[object Object]" I recommend you stick to identifiers, string literals, and number literals for property names
7.1.2 The Prototype Chain
In JavaScript every object has a prototype The property is internal and is referred
to as [[Prototype]] in the ECMA-262 specification It is an implicit reference to the prototypeproperty of the constructor that created the object For generic objects this corresponds to Object.prototype The prototype may have a prototype of
its own and so on, forming a prototype chain The prototype chain is used to share
properties across objects in JavaScript, and forms the basis for JavaScript’s inheri-tance model This concept is fundamentally different from classical inheriinheri-tance, in
1 Host objects will be discussed in Chapter 10, Feature Detection.
Trang 8which classes inherit from other classes, and objects constitute instances of classes
We’ll approach the subject by continuing our study of property access
When you read a property on an object, JavaScript uses the object’s internal [[Get]] method This method checks if the object has a property of the given
name If it has, its value is returned If the object does not have such a property,
the interpreter checks if the object has a [[Prototype]] that is not null (only
Object.prototypehas a null [[Prototype]]) If it does, the interpreter will
check whether the prototype has the property in question If it does, its value is
returned, otherwise the interpreter continues up the prototype chain until it reaches
Object.prototype If neither the object nor any of the objects in its prototype
has a property of the given name, undefined is returned
When you assign, or put, a value to an object property, the object’s internal [[Put]] method is used If the object does not already have a property of the given
name, one is created and its value is set to the provided value If the object already
has a property of the same name, its value is set to the one provided
Assignment does not affect the prototype chain In fact, if we assign a prop-erty that already exists on the prototype chain, we are shadowing the prototype’s
property Listing 7.3 shows an example of property shadowing To run the test with
JsTestDriver, set up a simple project as described in Chapter 3, Tools of the Trade,
and add a configuration file that loads test/*.js
Listing 7.3 Inheriting and shadowing properties
TestCase("ObjectPropertyTest", {
"test setting property shadows property on prototype":
function () { var object1 = {};
var object2 = {};
// Both objects inherit Object.prototype.toString assertEquals(object1.toString, object2.toString);
var chris = { name: "Chris", toString: function () { return this.name;
} };
// chris object defines a toString property that is // not the same object as object1 inherits from
Trang 9// Object.prototype assertFalse(object1.toString === chris.toString);
// Deleting the custom property unshadows the // inherited Object.prototype.toString
delete chris.toString;
assertEquals(object1.toString, chris.toString);
} });
As seen in Listing 7.3, object1 and object2 don’t define a toString property and so they share the same object—the Object.prototype
toString method—via the prototype chain The chris object, on the other hand, defines its own method, shadowing the toString property on the prototype chain If we delete the custom toString property from the chris object using the delete operator, the property no longer exists directly on the specific object, causing the interpreter to look up the method from the prototype chain, eventually finding Object.prototype
When we turn our attention to property attributes, we will discuss some addi-tional subtleties of the [[Put]] method
7.1.3 Extending Objects through the Prototype Chain
By manipulating the prototype property of JavaScript constructors we can mod-ify the behavior of every object created by it, including objects created before the manipulation This also holds for native objects, such as arrays To see how this works, we’re going to implement a simple sum method for arrays The test in Listing 7.4 illustrates what we want to achieve
Listing 7.4 Describing the behavior of Array.prototype.sum
TestCase("ArraySumTest", {
"test should summarize numbers in array": function () { var array = [1, 2, 3, 4, 5, 6];
assertEquals(21, array.sum());
} });
Running this test informs us that there is no sum method for arrays, which is not all that surprising The implementation is a trivial summarizing loop, as seen in Listing 7.5
Trang 10Listing 7.5 Adding a method to Array.prototype
Array.prototype.sum = function () {
var sum = 0;
for (var i = 0, l = this.length; i < l; i++) { sum += this[i];
} return sum;
};
Because all arrays inherit from Array.prototype, we’re able to add methods to all arrays But what happens if there already is a sum method for
arrays? Such a method could be provided by a given browser, a library or other
code running along with ours If this is the case, we’re effectively overwriting that
other method Listing 7.6 avoids this by placing our implementation inside an if
test that verifies that the method we’re adding does not already exist
Listing 7.6 Defensively adding a method to Array.prototype
if (typeof Array.prototype.sum == "undefined") {
Array.prototype.sum = function () { //
};
}
In general, this is a good idea when extending native objects or otherwise working on global objects This way we make sure our code doesn’t trip up other
code Even so, if there already is a sum method available, it may not act the way we
expect, causing our code that relies on our sum to break We can catch these errors
with a strong test suite, but this kind of problem clearly indicates that relying on
extensions to global objects may not be the best approach when the focus is writing
robust code
7.1.4 Enumerable Properties
Extending native prototypes like we just did comes with a price We already saw
how this may lead to conflicts, but there is another drawback to this approach
When adding properties to an object, they are instantly enumerable on any instance
that inherits it Listing 7.7 shows an example of looping arrays
Trang 11Listing 7.7 Looping arrays with for and for-in
TestCase("ArrayLoopTest", {
"test looping should iterate over all items":
function () { var array = [1, 2, 3, 4, 5, 6];
var result = [];
// Standard for-loop for (var i = 0, l = array.length; i < l; i++) { result.push(array[i]);
} assertEquals("123456", result.join(""));
},
"test for-in loop should iterate over all items":
function () { var array = [1, 2, 3, 4, 5, 6];
var result = [];
for (var i in array) { result.push(array[i]);
} assertEquals("123456", result.join(""));
} });
These two loops both attempt to copy all the elements of one array onto another, and then join both arrays into a string to verify that they do indeed contain the same elements Running this test reveals that the second test fails with the message in Listing 7.8
Listing 7.8 Result of running test in Listing 7.7
expected "123456" but was "123456function () { [ snip]"
To understand what’s happening, we need to understand the for-in enu-meration for (var property in object) will fetch the first enumerable property of object property is assigned the name of the property, and the body of the loop is executed This is repeated as long as object has more enu-merable properties, and the body of the loop does not issue break (or return if inside a function)