Phát triển Javascript - part 12 pps

10 235 0
Phát triển Javascript - part 12 pps

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

Thông tin tài liệu

ptg 5.3 Scope and Execution Context 83 In addition to the window property (in browsers), the global object can be accessed as this in the global scope. Listing 5.14 shows how window relates to the global object in browsers. Listing 5.14 The global object and window var global = this; TestCase("GlobalObjectTest", { "test window should be global object": function () { assertSame(global, window); assertSame(global.window, window); assertSame(window.window, window); } }); In the global scope, the global object is used as the variable object, meaning that declaring variables using the var keyword results in corresponding properties on the global object. In other words, the two assignments in Listing 5.15 are almost equivalent. Listing 5.15 Assigning properties on the global object var assert = function () { /* */ }; this.assert = function () { /* */ }; These two statements are not fully equivalent, because the variable declaration is hoisted, whereas the property assignment is not. 5.3.5 The Scope Chain Whenever a function is called, control enters a new execution context. This is even true for recursive calls to a function. As we’ve seen, the activation object is used for identifier resolution inside the function. In fact, identifier resolution occurs through the scope chain, which starts with the activation object of the current execution context. At the end of the scope chain is the global object. Consider the simple function in Listing 5.16. Calling it with a number results in a function that, when called, adds that number to its argument. Listing 5.16 A function that returns another function function adder(base) { return function (num) { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 84 Functions return base + num; }; } Listing 5.17 uses adder to create incrementing and decrementing functions. Listing 5.17 Incrementing and decrementing functions TestCase("AdderTest", { "test should add or subtract one from arg": function () { var inc = adder(1); var dec = adder(-1); assertEquals(3, inc(2)); assertEquals(3, dec(4)); assertEquals(3, inc(dec(3))); } }); The scope chain for the inc method contains its own activation object at the front. This object has a num property, corresponding to the formal parameter. The base variable, however, is not found on this activation object. When JavaScript does identifier resolution, it climbs the scope chain until it has no more objects. When base is not found, the next object in the scope chain is tried. The next object is the activation object created for adder, where in fact the base property is found. Had the property not been available here, identifier resolution would have continued on the next object in the scope chain, which in this case is the global object. If the identifier is not found on the global object, a reference error is thrown. Inside the functions created and returned from adder, the base variable is known as a free variable, which may live on after the adder function has finished executing. This behavior is also known as a closure, a concept we will dig deeper into in the next chapter, Chapter 6, Applied Functions and Closures. Functions created by the Function constructor have different scoping rules. Regardless of where they are created, these functions only have the global object in their scope chain, i.e., the containing scope is not added to their scope chain. This makes the Function constructor useful to avoid unintentional closures. 5.3.6 Function Expressions Revisited With a better understanding of the scope chain we can revisit function expressions and gain a better understanding of how they work. Function expressions can be use- ful when we need to conditionally define a function, because function declarations From the Library of WoweBook.Com Download from www.eBookTM.com ptg 5.3 Scope and Execution Context 85 are not allowed inside blocks, e.g., in an if-else statement. A common situation in which a function might be conditionally defined is when defining functions that will smooth over cross-browser differences, by employing feature detection. In Chapter 10, Feature Detection, we will discuss this topic in depth, and an example could be that of adding a trim function that trims strings. Some browsers offer the String.prototype.trim method, and we’d like to use this if it’s available. Listing 5.18 shows a possible way to implement such a function. Listing 5.18 Conditionally defining a function var trim; if (String.prototype.trim) { trim = function (str) { return str.trim(); }; } else { trim = function (str) { return str.replace(/^\s+|\s+$/g, ""); }; } Using function declarations in this case would constitute a syntax error as per the ECMAScript specification. However, most browsers will run the example in Listing 5.19. Listing 5.19 Conditional function declaration // Danger! Don't try this at home if (String.prototype.trim) { function trim(str) { return str.trim(); } } else { function trim(str) { return str.replace(/^\s+|\s+$/g, ""); } } When this happens, we always end up with the second implementation due to function hoisting—the function declarations are hoisted before executing the conditional statement, and the second implementation always overwrites the first. An exception to this behavior is found in Firefox, which actually allows function From the Library of WoweBook.Com Download from www.eBookTM.com ptg 86 Functions statments as a syntax extension. Syntax extensions are legal in the spec, but not something to rely on. The only difference between the function expressions and function declara- tions above is that the functions created with declarations have names. These names are useful both to call functions recursively, and even more so in debug- ging situations. Let’s rephrase the trim method and rather define it directly on the String.prototype object for the browsers that lack it. Listing 5.20 shows an updated example. Listing 5.20 Conditionally providing a string method if (!String.prototype.trim) { String.prototype.trim = function () { return this.replace(/^\s+|\s+$/g, ""); }; } With this formulation we can always trim strings using " string ".trim() regardless of whether the browser supports the method natively. If we build a large application by defining methods like this, wewillhavetroubledebugging it, because, e.g., Firebug stack traces will show a bunch of calls to anonymous functions, making it hard to navigate and use to locate the source of errors. Unit tests usually should have our backs, but readable stack traces are valuable at any rate. Named function expressions solve this problem, as Listing 5.21 shows. Listing 5.21 Using a named function expression if (!String.prototype.trim) { String.prototype.trim = function trim() { return this.replace(/^\s+|\s+$/g, ""); }; } Named function expressions differ somewhat from function declarations; the identifier belongs to the inner scope, and should not be visible in the defining scope. Unfortunately, Internet Explorer does not respect this. In fact, Internet Explorer does not do well with named function expressions at all, as side effects of the above example show in Listing 5.22. Listing 5.22 Named function expressions in Internet Explorer // Should throw a ReferenceError, true in IE assertFunction(trim); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 5.4 The this Keyword 87 if (!String.prototype.trim) { String.prototype.trim = function trim() { return this.replace(/^\s+|\s+$/g, ""); }; } // Should throw a ReferenceError, true in IE assertFunction(trim); // Even worse: IE creates two different function objects assertNotSame(trim, String.prototype.trim); This is a bleak situation; when faced with named function expressions, Internet Explorer creates two distinct function objects, leaks the identifier to the containing scope, and even hoists one of them. These discrepancies make dealing with named function expressions risky business that can easily introduce obscure bugs. By as- signing the function expression to a variable with the same name, the duplicated function object can be avoided (effectively overwritten), but the scope leak and hoisting will still be there. I tend to avoid named function expressions, favoring function declarations inside closures, utilizing different names for different branches if necessary. Of course, function declarations are hoisted and available in the containing scope as well—the difference is that this is expected behavior for function declarations, meaning no nasty surprises. The behavior of function declarations are known and predictable across browsers, and need no working around. 5.4 The this Keyword JavaScript’s this keyword throws many seasoned developers off. In most object oriented languages, this (or self) always points to the receiving object. In most object oriented languages, using this inside a method always means the object on which the method was called. This is not necessarily true in JavaScript, even though it is the default behavior in many cases. The method and method call in Listing 5.23 has this expected behavior. Listing 5.23 Unsurprising behavior of this var circle = { radius: 6, diameter: function () { return this.radius * 2; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 88 Functions } }; TestCase("CircleTest", { "test should implicitly bind to object": function () { assertEquals(12, circle.diameter()); } }); The fact that this.radius is a reference to circle.radius inside circle.diameter should not surprise you. The example in Listing 5.24 behaves differently. Listing 5.24 The this value is no longer the circle object "test implicit binding to the global object": function () { var myDiameter = circle.diameter; assertNaN(myDiameter()); // WARNING: Never ever rely on implicit globals // This is just an example radius = 2; assertEquals(4, myDiameter()); } This example reveals that it is the caller that decides the value of this. In fact, this detail was left out in the previous discussion about the execution context. In addition to creating the activation and variable objects, and appending to the scope chain, the this value is also decided when entering an execution context. this can be provided to a method either implicitly or explicitly. 5.4.1 Implicitly Setting this this is set implicitly when calling a function using parentheses; calling it as a function causes this to be set to the global object; calling it as a method causes this to be the object through which the function is called. “Calling the function as a method” should be understood as calling the function as a property of an object. This is a highly useful feature, because it allows JavaScript objects to share function objects and still have them execute on the right object. For instance, to borrow array methods for the arguments object as discussed previously, we can simply create a property on the object whose value is the method we want to execute, and execute it through arguments, implicitly setting this to arguments. Listing 5.25 shows such an example. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 5.4 The this Keyword 89 Listing 5.25 Calling a function as a method on an object function addToArray() { var targetArr = arguments[0]; arguments.slice = Array.prototype.slice; var add = arguments.slice(1); return targetArr.concat(add); } Calling the addToArray function will work exactly as the one presented in Listing 5.8. The ECMAScript specification specifically calls for many built-in meth- ods to be generic, allowing them to be used with other objects that exhibit the right qualities. For instance, the arguments object has both a length property and numeric indexes, which satisfies Array.prototype.slice’s requirements. 5.4.2 Explicitly Setting this When all we want is to control the value of this for a specific method call, it is much better to explicitly do so using the function’s call or apply methods. The Function.prototype.call method calls a function with the first ar- gument as this. Additional arguments are passed to the function when calling it. The first example of addToArray in Listing 5.8 used this method call Ar- ray.prototype.slice with arguments as this. Another example can be found in our previous circle example, as Listing 5.26 shows. Listing 5.26 Using call assertEquals(10, circle.diameter.call({ radius: 5 })); Here we pass an object literal that defines a radius property as the this when calling circle.diameter. 5.4.3 Using Primitives As this The first argument to call can be any object, even null. When passing null, the global object will be used as the this value. As we will see in Chapter 8, ECMAScript 5th Edition, this is about to change—in ECMAScript5 strict mode, passing null as the this value causes this to be null, not the global object. When passing primitive types, such as a string or boolean, as the this value, the value is wrapped in an object. This can be troublesome, e.g., when calling From the Library of WoweBook.Com Download from www.eBookTM.com ptg 90 Functions methods on booleans. Listing 5.27 shows an example in which this might produce unexpected results. Listing 5.27 Calling methods with booleans as this Boolean.prototype.not = function () { return !this; }; TestCase("BooleanTest", { "test should flip value of true": function () { assertFalse(true.not()); assertFalse(Boolean.prototype.not.call(true)); }, "test should flip value of false": function () { // Oops! Both fail, false.not() == false assertTrue(false.not()); assertTrue(Boolean.prototype.not.call(false)); } }); This method does not work as expected because the primitive booleans are converted to Boolean objects when used as this. Boolean coercion of an object always produces true, and using the unary logical not operator on true unsur- prisingly results in false. ECMAScript 5 strict mode fixes this as well, by avoiding the object conversion before using a value as this. The apply method is similar to call, except it only expects two arguments; the first argument is the this value as with call and its second argument is an array of arguments to pass to the function being called. The second argument does not need to be an actual array object; any array-like object will do, meaning that apply can be used to chain function calls by passing arguments as the second argument to apply. As an example, apply could be used to sum all numbers in an array. First con- sider the function in Listing 5.28, which accepts an arbitrary amount of arguments, assumes they’re numbers, and returns the sum. Listing 5.28 Summing numbers function sum() { var total = 0; for (var i = 0, l = arguments.length; i < l; i++) { total += arguments[i]; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 5.5 Summary 91 } return total; } Listing 5.29 shows two test cases for this method. The first test sums a series of numbers by calling the function with parentheses, whereas the second test sums an array of numbers via apply. Listing 5.29 Summing numbers with apply TestCase("SumTest", { "test should sum numbers": function () { assertEquals(15, sum(1, 2, 3, 4, 5)); assertEquals(15, sum.apply(null, [1, 2, 3, 4, 5])); } }); Remember, passing null as the first argument causes this to implicitly bind to the global object, which is also the case when the function is called as in the first test. ECMAScript 5 does not implicitly bind the global object, causing this to be undefined in the first call and null in the second. call and apply are invaluable tools when passing methods as callbacks to other functions. In the next chapter we will implement a companion method, Function.prototype.bind, which can bind an object as this to a given function without calling it immediately. 5.5 Summary In this chapter we have covered the theoretical basics of JavaScript functions. We have seen how to create functions, how to use them as objects, how to call them, and how to manipulate arguments and the this value. JavaScript functions differ from functions or methods in many other languages in that they are first class objects, and in the way the execution context and scope chain work. Also, controlling the this value from the caller may be an unfamiliar way to work with functions, but as we’ll see throughout this book, can be very useful. In the next chapter we will continue our look at functions and study some more interesting use cases as we dive into the concept known as closures. From the Library of WoweBook.Com Download from www.eBookTM.com ptg This page intentionally left blank From the Library of WoweBook.Com Download from www.eBookTM.com . inside blocks, e.g., in an if-else statement. A common situation in which a function might be conditionally defined is when defining functions that will smooth over cross-browser differences, by employing. function declara- tions above is that the functions created with declarations have names. These names are useful both to call functions recursively, and even more so in debug- ging situations the one presented in Listing 5.8. The ECMAScript specification specifically calls for many built-in meth- ods to be generic, allowing them to be used with other objects that exhibit the right qualities.

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

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

Tài liệu liên quan