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
192,66 KB
Nội dung
ptg 6 Applied Functions and Closures I n the previous chapter we discussed the theoretical aspects of JavaScript func- tions, familiarizing ourselves with execution contexts and the scope chain. JavaScript supports nested functions, which allows for closures that can keep private state, and can be used for anything from ad hoc scopesto implementing memoization, function binding, modules and stateful functions, and objects. In this chapter we will work through several examples of how to make good use of JavaScript functions and closures. 6.1 Binding Functions When passing methods as callbacks, the implicit this value is lost unless the object on which it should execute is passed along with it. This can be confusing unless the semantics of this are familiar. 6.1.1 Losing this: A Lightbox Example To illustrate the problem at hand, assume we have a “lightbox” object. A lightbox is simply an HTML element that is overlaid the page, and appears to float above the rest of the page, much like a popup, only with a web 2.0 name. In this example the lightbox pulls content from a URL and displays it in a div element. For convenience, an anchorLightbox function is provided, which turns an anchor element into a 93 From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 94 Applied Functions and Closures lightbox toggler; when the anchor is clicked, the page it links to is loaded into a div that is positioned above the current page. Listing 6.1 shows a rough outline. Listing 6.1 Lightbox pseudo code var lightbox = { open: function () { ajax.loadFragment(this.url, { target: this.create() }); return false; }, close: function () { /* */ }, destroy: function () { /* */ }, create: function () { /* Create or return container */ } }; function anchorLightbox(anchor, options) { var lb = Object.create(lightbox); lb.url = anchor.href; lb.title = anchor.title || anchor.href; Object.extend(lb, options); anchor.onclick = lb.open; return lb; } Note that the code will not run as provided; it’s simply a conceptual exam- ple. The details of Object.create and Object.extend will be explained in Chapter 7, Objects and Prototypal Inheritance, and the ajax.loadFragment method can be assumed to load the contents of a URL into the DOM element specified by the target option. The anchorLightbox function creates a new object that inherits from the lightbox object, sets crucial properties, and returns the new object. Additionally, it assigns an event handler for the click event. Using DOM0 event properties will do for now but is generally not advisable; we’ll see a better way to add event handlers in Chapter 10, Feature Detection. Unfortunately, the expected behavior fails when the link is clicked. The reason is that when we assign the lb.open method as the event handler, we lose the implicit binding of this to the lb object, which only occurs when the function is From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 6.1 Binding Functions 95 called as a property of it. In the previous chapter we saw how call and apply can be used to explicitly set the this value when calling a function. However, those methods only help at call time. 6.1.2 Fixing this via an Anonymous Function To work around the problem, we could assign an anonymous function as the event handler that when executed calls the open method, making sure the correct this value is set. Listing 6.2 shows the workaround. Listing 6.2 Calling open through an anonymous proxy function function anchorLightbox(anchor, options) { /* */ anchor.onclick = function () { return lb.open(); }; /* */ } Assigning the inner function as the event handler creates a closure. Normally, when a function exits, the execution context along with its activation and variable object are no longer referenced, and thus are available for garbage collection. How- ever, the moment we assign the inner function as the event handler something in- teresting happens. Even after the anchorLightbox finishes, the anchor object, through its onclick property, still has access to the scope chain of the execution context created for anchorLightbox. The anonymous inner function uses the lb variable, which is neither a parameter nor a local variable; it is a free variable, accessible through the scope chain. Using the closure to handle the event, effectively proxying the method call, the lightbox anchor should now work as expected. However, the manual wrapping of the method call doesn’t feel quite right. If we were to define several event handlers in the same way, we would introduce duplication, which is error-prone and likely to make code harder to maintain, change, and understand. A better solution is needed. 6.1.3 Function.prototype.bind ECMAScript 5 provides the Function.prototype.bind function, which is also found in some form in most modern JavaScript libraries. The bind method accepts an object as its first argument and returns a function object that, when From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 96 Applied Functions and Closures called, calls the original function with the bound object as the this value. In other words, it provides the functionality we just implemented manually, and could be considered the deferred companion to call and apply. Using bind, we could update anchorLightbox as shown in Listing 6.3. Listing 6.3 Using bind function anchorLightbox(anchor, options) { /* */ anchor.onclick = lb.open.bind(lb); /* */ } Because not all browsers yet implement this highly useful function, we can conditionally provide our own implementation for those browsers that lack it. Listing 6.4 shows a simple implementation. Listing 6.4 Implementation of bind if (!Function.prototype.bind) { Function.prototype.bind = function (thisObj) { var target = this; return function () { return target.apply(thisObj, arguments); }; }; } The implementation returns a function—a closure—that maintains its reference to the thisObj argument and the function itself. When the returned function is executed, the original function is called with this explicitly set to the bound object. Any arguments passed to the returned function is passed on to the original function. Adding the function to Function.prototype means it will be available as a method on all function objects, so this refers to the function on which the method is called. In order to access this value we need to store it in a local variable in the outer function. As we saw in the previous chapter, this is calculated upon entering a new execution context and is not part of the scope chain. Assigning it to a local variable makes it accessible through the scope chain in the inner function. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 6.1 Binding Functions 97 6.1.4 Binding with Arguments According to the ECMAScript 5 specification (and, e.g., the Prototype.js implemen- tation), bind should support binding functions to arguments as well as the this value. Doing so means we can “prefill” a function with arguments, bind it to an object, and pass it along to be called at some later point. This can prove extremely useful for event handlers, in cases in which the handling method needs arguments known at bind time. Another useful case for binding arguments is deferring some computation, e.g., by passing a callback to setTimeout. Listing 6.5 shows an example in which bind is used to prefill a function with arguments to defer a benchmark with setTimeout. The bench function calls the function passed to it 10,000 times and logs the result. Rather than manually carrying out the function calls in an anonymous function passed to setTimeout, we use bind to run all the benchmarks in the benchmarks array by binding the forEach method to the array and the bench function as its argument. Listing 6.5 Deferring a method call using bind and setTimeout function bench(func) { var start = new Date().getTime(); for (var i = 0; i < 10000; i++) { func(); } console.log(func, new Date().getTime() - start); } var benchmarks = [ function forLoop() { /* */ }, function forLoopCachedLength() { /* */ }, /* */ ]; setTimeout(benchmarks.forEach.bind(benchmarks, bench), 500); The above listing will cause the benchmarks to be run after 500 milliseconds. The keen reader will recognize the benchmarks from Chapter 4, Test to Learn. Listing 6.6 shows one possible way of implementing bind such that it allows arguments bound to the function as well as the this value. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 98 Applied Functions and Closures Listing 6.6 bind with arguments support if (!Function.prototype.bind) { Function.prototype.bind = function (thisObj) { var target = this; var args = Array.prototype.slice.call(arguments, 1); return function () { var received = Array.prototype.slice.call(arguments); return target.apply(thisObj, args.concat(received)); }; }; } This implementation is fairly straightforward. It keeps possible arguments passed to bind in an array, and when the bound function is called it concatenates this array with possible additional arguments received in the actual call. Although simple, the above implementation is a poor performer. It is likely that bind will be used most frequently to simply bind a function to an object, i.e., no arguments. In this simple case, converting and concatenating the arguments will only slow down the call, both at bind time and at call time for the bound function. Fortunately, optimizing the different cases is pretty simple. The different cases are: • Binding a function to an object, no arguments • Binding a function to an object and one or more arguments • Calling a bound function without arguments • Calling a bound function with arguments The two latter steps occur for both of the former steps, meaning that there are two cases to cater for at bind time, and four at call time. Listing 6.7 shows an optimized function. Listing 6.7 Optimized bind if (!Function.prototype.bind) { (function () { var slice = Array.prototype.slice; Function.prototype.bind = function (thisObj) { var target = this; if (arguments.length > 1) { From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 6.1 Binding Functions 99 var args = slice.call(arguments, 1); return function () { var allArgs = args; if (arguments.length > 0) { allArgs = args.concat(slice.call(arguments)); } return target.apply(thisObj, allArgs); }; } return function () { if (arguments.length > 0) { return target.apply(thisObj, arguments); } return target.call(thisObj); }; }; }()); } This implementation is somewhat more involved, but yields much better per- formance, especially for the simple case of binding a function to an object and no arguments and calling it with no arguments. Note that the implementation given here is missing one feature from the EC- MAScript 5 specification. The spec states that the resulting function should behave as the bound function when used in a new expression. 6.1.5 Currying Currying is closely related to binding, because they both offer a way to partially apply a function. Currying differs from binding in that it only pre-fills arguments; it does not set the this value. This is useful, because it allows us to bind arguments to functions and methods while maintaining their implicit this value. The implicit this allows us to use currying to bind arguments to functions on an object’s proto- type, and still have the function execute with a given object as its this value. Listing 6.8 shows an example of implementing String.prototype.trim in terms of String.prototype.replace using Function.prototype.curry. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 100 Applied Functions and Closures Listing 6.8 Implementing a method in terms of another one and curry (function () { String.prototype.trim = String.prototype.replace.curry(/^\s+|\s+$/g, ""); TestCase("CurryTest", { "test should trim spaces": function () { var str = " some spaced string "; assertEquals("some spaced string", str.trim()); } }); }()); The implementation of curry in Listing 6.9 resembles the bind implementa- tion from before. Listing 6.9 Implementing curry if (!Function.prototype.curry) { (function () { var slice = Array.prototype.slice; Function.prototype.curry = function () { var target = this; var args = slice.call(arguments); return function () { var allArgs = args; if (arguments.length > 0) { allArgs = args.concat(slice.call(arguments)); } return target.apply(this, allArgs); }; }; }()); } There’s no optimization for the case in which curry does not receive argu- ments, because calling it without arguments is senseless and should be avoided. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 6.2 Immediately Called Anonymous Functions 101 6.2 Immediately Called Anonymous Functions A common practice in JavaScript is to create anonymous functions that are imme- diately called. Listing 6.10 shows a typical incarnation. Listing 6.10 An immediately called anonymous function (function () { /* */ }()); The parentheses wrapping the entire expression serves two purposes. Leaving them out causes the function expression to be seen as a function declaration, which would constitute a syntax error without an identifier. Furthermore, expressions (as opposed to declarations) cannot start with the word “function” as it might make them ambiguous with function declarations, so giving the function a name and calling it would not work either. Thus, the parentheses are necessary to avoid syntax errors. Additionally, when assigning the return value of such a function to a variable, the leading parentheses indicates that the function expression is not what’s returned from the expression. 6.2.1 Ad Hoc Scopes JavaScript only has global scope and function scope, which may sometimes cause weird problems. The first problem we need to avoid is leaking objects into the global scope, because doing so increases our chances of naming collisions with other scripts, such as third party libraries, widgets, and web analytics scripts. 6.2.1.1 Avoiding the Global Scope We can avoid littering the global scope with temporary variables (e.g., loop variables and other intermittent variables) by simply wrapping our code in a self-executing closure. Listing 6.11 shows an example of using the aforementioned lightbox object; every anchor element in the document with the class name lightbox is picked up and passed to the anchorLightbox function. Listing 6.11 Creating lightboxes (function () { var anchors = document.getElementsByTagName("a"); var regexp = /(^|\s)lightbox(\s|$)/; for (var i = 0, l = anchors.length; i < l; i++) { From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg 102 Applied Functions and Closures if (regexp.test(anchors[i].className)) { anchorLightbox(anchors[i]); } } }()); 6.2.1.2 Simulating Block Scope Another useful case for immediately called closures is when creating closures inside loops. Assume that we had opted for a different design of our lightbox widget, in which there was only one object, and it could be used to open any number of lightboxes. In this case we would need to add event handlers manually, as in Listing 6.12. Listing 6.12 Adding event handlers the wrong way (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)) { anchors[i].onclick = function () { controller.open(anchors[i]); return false; }; } } }()); This example will not work as expected. The event handler attached to the links forms a closure that can access the variables local to the outer function. However, all the closures (one for each anchor) keep a reference to the same scope; clicking any of the anchors will cause the same lightbox to open. When the event handler for an anchor is called, the outer function has changed the value of i since it was assigned, thus it will not trigger the correct lightbox to open. To fix this we can use a closure to capture the anchor we want to associate with the event handler by storing it in a variable that is not available to the outer function, and thus cannot be changed by it. Listing 6.13 fixes the issue by passing the anchor as argument to a new closure. From the Library of WoweBook.Com Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. [...]... 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: {} };... tddjs.namespace("nstest.nested"); assertSame(tddjs.nstest.nested, result); }, "test only create missing parts": Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark From the Library of WoweBook.Com 105 6.2 Immediately Called Anonymous Functions function () { var existing = {}; tddjs.nstest = { nested: { existing: existing } }; var result = tddjs.namespace("nstest.nested.ui");... 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({}); Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark From the Library of WoweBook.Com 108 Applied Functions and Closures assertNumber(id); }, "test should return consistent id... 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]; Please purchase PDF Split-Merge... 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... 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... Anonymous Functions 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... 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... 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 =... 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) . 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. { 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":