Phát triển Javascript - part 14 ppsx

10 243 0
Phát triển Javascript - part 14 ppsx

Đang tải... (xem toàn văn)

Thông tin tài liệu

ptg 6.2 Immediately Called Anonymous Functions 103 Listing 6.13 Fixing scoping issues with nested closures (function () { var anchors = document.getElementsByTagName("a"); var controller = Object.create(lightboxController); var regexp = /(^|\s)lightbox(\s|$)/; for (var i = 0, l = anchors.length; i < l; i++) { if (regexp.test(anchors[i].className)) { (function (anchor) { anchor.onclick = function () { controller.open(anchor); return false; }; }(anchors[i])); } } }()); anchor is now a formal parameter to the inner closure, whose variable object cannot be accessed or tampered with by the containing scope. Thus, the event handlers will work as expected. Examples aside, closures in loops are generally a performance issue waiting to happen. Most problems can be better solved by avoiding the nested closure, for instance, by using dedicated functions to create the closure like we did in Listing 6.11. When assigning event handlers, there is even another problem with nesting functionslike this,because thecircular referencebetween theDOM element and its event handler may cause memory leaks. 6.2.2 Namespaces A good strategy to stay out of the global scope is to use some kind of namespacing. JavaScript does not have native namespaces, but becauseit offers such useful objects and functions it does not need them either. To use objects as namespaces, simply define a single object in the global scope and implement additional functions and objects as properties of it. Listing 6.14 shows how we could possibly implement the lightbox object inside our own tddjs namespace. Listing 6.14 Using objects as namespaces var tddjs = { lightbox: { /* */ }, From the Library of WoweBook.Com Download from www.eBookTM.com ptg 104 Applied Functions and Closures anchorLightbox: function (anchor, options) { /* */ } }; In larger libraries, we might want better organization than simply defining everything inside the same object. For example, the lightbox might live in tddjs. ui, whereas ajax functionality could live in tddjs.ajax. Many libraries provide some kind of namespace function to help with this kind of organizing. Organizing all code inside a single file is not a sound strategy, and when splitting code inside the same object across several files, knowing if the namespace object is already created becomes an issue. 6.2.2.1 Implementing Namespaces For this book we will use the tddjs object to namespace reusable code that is shared between chapters. To help with namespacing we will implement our own function that will loop each level in the namespace—provided as a string—creating objects that don’t exist. Listing 6.15 shows a few test cases demonstrating its use and side-effects. Listing 6.15 Demonstrating the namespace function TestCase("NamespaceTest", { tearDown: function () { delete tddjs.nstest; }, "test should create non-existent object": function () { tddjs.namespace("nstest"); assertObject(tddjs.nstest); }, "test should not overwrite existing objects": function () { tddjs.nstest = { nested: {} }; var result = tddjs.namespace("nstest.nested"); assertSame(tddjs.nstest.nested, result); }, "test only create missing parts": From the Library of WoweBook.Com Download from www.eBookTM.com ptg 6.2 Immediately Called Anonymous Functions 105 function () { var existing = {}; tddjs.nstest = { nested: { existing: existing } }; var result = tddjs.namespace("nstest.nested.ui"); assertSame(existing, tddjs.nstest.nested.existing); assertObject(tddjs.nstest.nested.ui); } }); namespace is expected to be implemented as a method on the global tddjs object, and manages namespaces inside it. This way tddjs is completely sandboxed inside its own namespace, and using it along with immediately called closures will ensure we don’t leak properties to the global object. Its implementation is found in Listing 6.16. Save it in a file called tdd.js; we will add more utilities to this file/namespace throughout the book. Listing 6.16 The namespace function var tddjs = (function () { function namespace(string) { var object = this; var levels = string.split("."); for (var i = 0, l = levels.length; i < l; i++) { if (typeof object[levels[i]] == "undefined") { object[levels[i]] = {}; } object = object[levels[i]]; } return object; } return { namespace: namespace }; }()); This implementation shows a few interesting uses of functions. It wraps the entire implementation in a closure, returning an object literal that is assigned to the global tddjs object. Avoiding the trouble with named function expressions and taking advantage of the fact that the closure creates a local scope, we define namespace using a From the Library of WoweBook.Com Download from www.eBookTM.com ptg 106 Applied Functions and Closures function declaration and then assign it to the namespace property of the returned object. The namespace function starts resolving namespaces from this. Doing so allows the function to be easily borrowed to create namespaces in other objects than tddjs. Listing 6.17 shows an example of borrowing the method to create namespaces inside another object. Listing 6.17 Creating custom namespaces "test namespacing inside other objects": function () { var custom = { namespace: tddjs.namespace }; custom.namespace("dom.event"); assertObject(custom.dom.event); assertUndefined(tddjs.dom); } As the test shows, the tddjs object is not modified when calling the method through another object, which should not be surprising. 6.2.2.2 Importing Namespaces When organizing code in namespaces, we might tire from all the typing. Program- mers are lazy creatures, and typing tddjs.ajax.request might be too much to ask. As we already saw, JavaScript does not have native namespaces, and so there is no import keyword to import a set of objects into the local scope. Luckily, closures have local scope, which means that we can simply assign nested objects to local variables to “import” them. Listing 6.18 shows an example. Listing 6.18 Using a local variable to “import” a namespace (function () { var request = tddjs.ajax.request; request(/* */); /* */ }()); Another advantage of this technique is that, unlike with global variables, local variable identifiers can safely be minified. Thus, using local aliases can help reduce the size of scripts in production as well. Be careful when making local aliases to methods as in the above example. If the method is dependent on its this object, such local importing effectively breaks From the Library of WoweBook.Com Download from www.eBookTM.com ptg 6.3 Stateful Functions 107 implicit binding. Because importing namespaces effectively caches the object inside the closure, it can also cause trouble when trying to mock or stub the imported object. Using namespaces is a highly useful way to organize code in a clean way without tripping up the global namespace. You might worry that the property lookups come with a performance penalty, which they do, but compared with, e.g., DOM manipulation, the impact of these namespaces will be minute. 6.3 Stateful Functions A closure can maintain state through its free variables. The scope chain that allows access to these free variables is only accessible from within the scope chain itself, which means that free variables by definition are private. In Chapter 7, Objects and Prototypal Inheritance, we will see how this can be used to create objects with private state, a feature not otherwise offered by JavaScript (i.e., no private keyword), in a pattern popularized as “the module pattern.” In this section we will use closures to hide implementation details for functions. 6.3.1 Generating Unique Ids The ability to generate unique ids for any given object is useful whenever we want to use objects and functions as, e.g., property keys in objects. As we’ll see in the next chapter, property identifiers in JavaScript are always coerced to strings; so even though we can set a property whose key is an object, it won’t do what we expect. Another useful application of unique ids in the case of DOM elements. Storing data as properties of DOM elements can cause memory leaks and other undesired behavior. One way to avoid these problems, currently employed by most major libraries, is to generate a unique id for an element, and keep an element storage separate from the element. This allows for an API that can get and set data on the element without actually storing data other than the unique id directly on it. As an example of a stateful closure, we will implement a tddjs.uid method. The method accepts an object and returns a numeric id, which is stored in a property on the object. Listing 6.19 shows a few test cases describing its behavior. Listing 6.19 Specification of the uid function TestCase("UidTest", { "test should return numeric id": function () { var id = tddjs.uid({}); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 108 Applied Functions and Closures assertNumber(id); }, "test should return consistent id for object": function () { var object = {}; var id = tddjs.uid(object); assertSame(id, tddjs.uid(object)); }, "test should return unique id": function () { var object = {}; var object2 = {}; var id = tddjs.uid(object); assertNotEquals(id, tddjs.uid(object2)); }, "test should return consistent id for function": function () { var func = function () {}; var id = tddjs.uid(func); assertSame(id, tddjs.uid(func)); }, "test should return undefined for primitive": function () { var str = "my string"; assertUndefined(tddjs.uid(str)); } }); The tests can be run with JsTestDriver, as described in Chapter 3, Tools of the Trade. This is not an exhaustive test suite, but it shows the basic behavior the method will support. Note that passing primitives to the function will not work as assigning properties to primitives does not actually add properties to the primitive— the primitive is wrapped in an object for the property access, which is immediately thrown away, i.e., new String("my string"). __ uid = 3. The implementation is the interesting part. The uid method generates ids by looking up a counter that is incremented every time an id is requested. We could store this id as a property of the uid function object, but that would make it susceptible From the Library of WoweBook.Com Download from www.eBookTM.com ptg 6.3 Stateful Functions 109 to outside modification, which could cause the method to return the same id twice, breaking its contract. By using a closure, we can store the counter in a free variable that is protected from outside access. Listing 6.20 shows the implementation. Listing 6.20 Storing state in a free variable (function () { var id = 0; function uid(object) { if (typeof object.__uid != "number") { object.__uid = id++; } return object.__uid; } if (typeof tddjs == "object") { tddjs.uid = uid; } }()); The implementation uses an immediately called anonymous closure to create a scope in which the id variable can live. The uid function, which has access to this variable, is exposed to the world as the tddjs.uid method. The typeof check avoids a reference error if for some reason the file containing the tddjs object has not loaded. 6.3.2 Iterators Iterators are objects that encapsulate the enumeration of a collection object. They provide a consistent API to traverse any kind of collection, and can provide better control over iteration than what simple for and while loops can, e.g., by ensuring that an item is never accessed more than once, that items are accessed strictly sequential and more. Closures can be used to implement iterators rather effortlessly in JavaScript. Listing 6.21 shows the basic behavior of the iterators created by tddjs.iterator. Listing 6.21 Behavior of the tddjs.iterator method TestCase("IteratorTest", { "test next should return first item": function () { var collection = [1, 2, 3, 4, 5]; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 110 Applied Functions and Closures var iterator = tddjs.iterator(collection); assertSame(collection[0], iterator.next()); assertTrue(iterator.hasNext()); }, "test hasNext should be false after last item": function () { var collection = [1, 2]; var iterator = tddjs.iterator(collection); iterator.next(); iterator.next(); assertFalse(iterator.hasNext()); }, "test should loop collection with iterator": function () { var collection = [1, 2, 3, 4, 5]; var it = tddjs.iterator(collection); var result = []; while (it.hasNext()) { result.push(it.next()); } assertEquals(collection, result); } }); A possible implementation of the iterator is shown in Listing 6.22. Listing 6.22 Possible implementation of tddjs.iterator (function () { function iterator(collection) { var index = 0; var length = collection.length; function next() { var item = collection[index++]; return item; } function hasNext() { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 6.3 Stateful Functions 111 return index < length; } return { next: next, hasNext: hasNext }; } if (typeof tddjs == "object") { tddjs.iterator = iterator; } }()); The overall pattern should start to look familiar. The interesting parts are the collection, index and length free variables. The iterator function re- turns an object whose methods have access to the free variables, and is an imple- mentation of the module pattern mentioned previously. The iterator interface was purposely written to imitate that of Java’s iter- ators. However, JavaScript’s functions have more to offer, and this interface could be written in a much leaner way, as seen in Listing 6.23. Listing 6.23 Functional iterator approach (function () { function iterator(collection) { var index = 0; var length = collection.length; function next() { var item = collection[index++]; next.hasNext = index < length; return item; } next.hasNext = index < length; return next; } if (typeof tddjs == "object") { tddjs.iterator = iterator; } }()); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 112 Applied Functions and Closures This implementation simply returns the next function, and assigns hasNext as a property of it. Every call to next updates the hasNext property. Leveraging this we can update the loop test to look like Listing 6.24. Listing 6.24 Looping with functional iterators "test should loop collection with iterator": function () { var collection = [1, 2, 3, 4, 5]; var next = tddjs.iterator(collection); var result = []; while (next.hasNext) { result.push(next()); } assertEquals(collection, result); } 6.4 Memoization Our final closure example will be provided by memoization, a caching technique at the method level and a popular example of the power of JavaScript functions. Memoization is a technique that can be employed to avoid carrying out ex- pensive operations repeatedly, thus speeding up programs. There are a few ways to implement memoization in JavaScript, and we’ll start with the one closest to the examples we’ve worked with so far. Listing 6.25 shows an implementation of the Fibonacci sequence, which uses two recursive calls to calculate the value at a given point in the sequence. Listing 6.25 The Fibonacci sequence function fibonacci(x) { if (x < 2) { return 1; } return fibonacci(x - 1) + fibonacci(x - 2); } The Fibonacci sequence is very expensive, and quickly spawns too many recur- sive calls for a browser to handle. By wrapping the function in a closure, we can manually memoize values to optimize this method, as seen in Listing 6.26. From the Library of WoweBook.Com Download from www.eBookTM.com . The interesting parts are the collection, index and length free variables. The iterator function re- turns an object whose methods have access to the free variables, and is an imple- mentation of. (x < 2) { return 1; } return fibonacci(x - 1) + fibonacci(x - 2); } The Fibonacci sequence is very expensive, and quickly spawns too many recur- sive calls for a browser to handle. By wrapping. namespaces, we might tire from all the typing. Program- mers are lazy creatures, and typing tddjs.ajax.request might be too much to ask. As we already saw, JavaScript does not have native namespaces, and

Ngày đăng: 04/07/2014, 22:20

Từ khóa liên quan

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan