Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 20 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
20
Dung lượng
202,07 KB
Nội dung
ptg 6.4 Memoization 113 Listing 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]; }; }; } From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 114 Applied Functions and Closures This 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 From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 6.5 Summary 115 arguments. 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. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg This page intentionally left blank From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 7 Objects and Prototypal Inheritance J avaScript 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" 117 From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 118 Objects and Prototypal Inheritance }, 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. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 7.1 Objects and Properties 119 Listing 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 objects 1 ), 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 prototype property 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 inheritance, in 1. Host objects will be discussed in Chapter 10, Feature Detection. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 120 Objects and Prototypal Inheritance which 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.prototype has 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 From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 7.1 Objects and Properties 121 // 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. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 122 Objects and Prototypal Inheritance Listing 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. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. [...]... and Properties Listing 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 () {... 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 enumeration... the properties on the respective prototype chains with DontEnum will magically disappear from a for-in loop, as seen in Listing 7.11 Listing 7.11 Overriding properties with DontEnum TestCase("PropertyEnumerationTest", { "test should enumerate shadowed object properties": function () { var object = { // Properties with DontEnum on Object.prototype toString: "toString", toLocaleString: "toLocaleString",... object[property]); } } } }; }()); If we change the for-in loops in the tests in Listing 7.11 to use the new tddjs.each method, the tests will run, even on Internet Explorer Additionally, the method smoothes over a similar bug in Chrome in which function objects prototype property is not enumerable when shadowed 7.2 Creating Objects with Constructors JavaScript functions have the ability to act as constructors... instanceof operator to determine the relationship between objects Additionally, we can use the constructor property to inspect their origin, as seen in Listing 7.14 Listing 7.14 Inspecting objects TestCase("CircleTest", { "test inspect objects": function () { var circ = new Circle(6); var circ2 = { radius: 6 }; assertTrue(circ instanceof Object); assertTrue(circ instanceof Circle); assertTrue(circ2 instanceof... commonly Function.prototype All JavaScript objects have an internal [[Prototype]] property; only function objects have the prototype property Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark From the Library of WoweBook.Com 131 7.2 Creating Objects with Constructors 7.2.2 Creating Objects with new When a function is called with the new operator, a new JavaScript object is created... decide to do so 7.1.5 Property Attributes ECMA-262 defines four properties that may be set for any given property It is important to note that these attributes are set for properties by the interpreter, but JavaScript code you write has no way of setting these attributes The ReadOnly and DontDelete attributes cannot be inspected explicitly, but we can deduce their values ECMA-262 specifies the Object.prototype... we add properties to an object or one of the objects in its prototype chain, they are enumerable by default Because of this fact, these new properties will also appear in a for-in loop, as shown by the test failure above I recommend you don’t use for-in on arrays The problem illustrated above can be worked around, as we will see shortly, but not without trading off performance Using for-in on arrays... "isPrototypeOf", propertyIsEnumerable: "propertyIsEnumerable", constructor: "constructor" }; var result = []; for (var property in object) { result.push(property); } assertEquals(7, result.length); }, "test should enumerate shadowed function properties": function () { var object = function () {}; // Additional properties with DontEnum on //Function.prototype object.prototype = "prototype"; object.call... watermark From the Library of WoweBook.Com 128 Objects and Prototypal Inheritance var result = []; for (var property in object) { result.push(property); } assertEquals(3, result.length); } }); Both of these tests fail in all versions of Internet Explorer, including IE8; result.length is 0 We can solve this issue by making a special case for the non-enumerable properties on Object.prototype as well as Function . 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. }; 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. 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