1. Trang chủ
  2. » Công Nghệ Thông Tin

Phát triển Javascript - part 17 potx

10 172 0

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

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 10
Dung lượng 2,35 MB

Nội dung

ptg 7.2 Creating Objects with Constructors 133 Circle.prototype.area = function () { return this.radius * this.radius * Math.PI; }; Listing 7.17 shows a simple test to verify that objects do indeed inherit the methods. Listing 7.17 Testing Circle.prototype.diameter "test should inherit properties from Circle.prototype": function () { var circle = new Circle(6); assertEquals(12, circle.diameter()); } Repeating Circle.prototype quickly becomes cumbersome and expensive (in terms of bytes to go over the wire) when adding more than a few properties to the prototype. We can improve this pattern in a number of ways. Listing 7.18 shows the shortest way—simply provide an object literal as the new prototype. Listing 7.18 Assigning Circle.prototype Circle.prototype = { diameter: function () { return this.radius * 2; }, circumference: function () { return this.diameter() * Math.PI; }, area: function () { return this.radius * this.radius * Math.PI; } }; Unfortunately, this breaks some of our previous tests. In particular, the assertion in Listing 7.19 no longer holds. Listing 7.19 Failing assertion on constructor equality assertEquals(Circle, circle.constructor) From the Library of WoweBook.Com Download from www.eBookTM.com ptg 134 Objects and Prototypal Inheritance When we assign a new object to Circle.prototype, JavaScript no longer creates a constructor property for us. This means that the [[Get]] for con- structor will go up the prototype chain until a value is found. In the case of our constructor, the result is Object.prototype whose constructor property is Object, as seen in Listing 7.20. Listing 7.20 Broken constructor property "test constructor is Object when prototype is overridden": function () { function Circle() {} Circle.prototype = {}; assertEquals(Object, new Circle().constructor); } Listing 7.21 solves the problem by assigning the constructor property manually. Listing 7.21 Fixing the missing constructor property Circle.prototype = { constructor: Circle, // }; To avoid the problem entirely, we could also extend the given prototype prop- erty in a closure to avoid repeating Circle.prototype for each property. This approach is shown in Listing 7.22. Listing 7.22 Avoiding the missing constructor problem (function (p) { p.diameter = function () { return this.radius * 2; }; p.circumference = function () { return this.diameter() * Math.PI; }; p.area = function () { return this.radius * this.radius * Math.PI; }; }(Circle.prototype)); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 7.2 Creating Objects with Constructors 135 By not overwriting the prototype property, we are also avoiding its constructor property being enumerable. The object provided for us has the DontEnum attribute set, which is impossible to recreate when we assign a cus- tom object to the prototype property and manually restore the constructor property. 7.2.4 The Problem with Constructors There is a potential problem with constructors. Because there is nothing that sep- arates a constructor from a function, there is no guarantee that someone won’t use your constructor as a function. Listing 7.23 shows how a missing new keyword can have grave effects. Listing 7.23 Constructor misuse "test calling prototype without 'new' returns undefined": function () { var circle = Circle(6); assertEquals("undefined", typeof circle); // Oops! Defined property on global object assertEquals(6, radius); } This example shows two rather severe consequences of calling the constructor as a function. Because the constructor does not have a return statement, the type of the circle ends up being undefined. Even worse, because we did not call the function with the new operator, JavaScript did not create a new object and set it as the function’s this value for us. Thus, the function executes on the global object, causing this.radius = radius to set a radius property on the global object, as shown by the second assertion in Listing 7.23. In Listing 7.24 the problem is mitigated by use of the instanceof operator. Listing 7.24 Detecting constructor misuse function Circle(radius) { if (!(this instanceof Circle)) { return new Circle(radius); } this.radius = radius; } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 136 Objects and Prototypal Inheritance Whenever someone forgets the new operator when calling the constructor, this will refer to the global object rather than a newly created object. By using the instanceof operator, we’re able to catch this, and can explicitly call the constructor over again with the same arguments and return the new object. ECMA-262 defines the behavior for all the native constructors when used as functions. The result of calling a constructor as a function often has the same effect as our implementation above—a new object is created as if the function was actually called with the new operator. Assuming that calling a constructor without new is usually a typo, you may want to discourage using constructors as functions. Throwing an error rather than sweeping the error under the rug will probably ensure a more consistent code base in the long run. 7.3 Pseudo-classical Inheritance Equipped with our understanding of constructors and their prototype properties, we can now create arbitrary hierarchies of objects, in much the same way one would create class hierarchies in a classical language. We will do this with Sphere, a constructor whose prototype property inherits from Circle.prototype rather than Object.prototype. We need Sphere.prototype to refer to an object whose internal [[Pro- totype]] is Circle.prototype. In other words, we need a circle object to set up this link. Unfortunately, this process is not straightforward; In order to create a circle object we need to invoke the Circle constructor. However, the constructor may provide our prototype object with unwanted state, and it may even fail in the absence of input arguments. To circumvent this potential problem, Listing 7.25 uses an intermediate constructor that borrows Circle.prototype. Listing 7.25 Deeper inheritance function Sphere(radius) { this.radius = radius; } Sphere.prototype = (function () { function F() {}; F.prototype = Circle.prototype; return new F(); }()); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 7.3 Pseudo-classical Inheritance 137 // Don't forget the constructor - else it will resolve as // Circle through the prototype chain Sphere.prototype.constructor = Sphere; Now we can create spheres that inherit from circles, as shown by the test in Listing 7.26. Listing 7.26 Testing the new Sphere constructor "test spheres are circles in 3D": function () { var radius = 6; var sphere = new Sphere(radius); assertTrue(sphere instanceof Sphere); assertTrue(sphere instanceof Circle); assertTrue(sphere instanceof Object); assertEquals(12, sphere.diameter()); } 7.3.1 The Inherit Function In Listing 7.25 we extended the Sphere constructor with the Circle construc- tor by linking their prototypes together, causing sphere objects to inherit from Circle.prototype. The solution is fairly obscure, especially when compared with inheritance in other languages. Unfortunately, JavaScript does not offer any abstractions over this concept, but we are free to implement our own. Listing 7.27 shows a test for what such an abstraction might look like. Listing 7.27 Specification for inherit TestCase("FunctionInheritTest", { "test should link prototypes": function () { var SubFn = function () {}; var SuperFn = function () {}; SubFn.inherit(SuperFn); assertTrue(new SubFn() instanceof SuperFn); } }); We already implemented this feature in Listing 7.25, so we only need to move it into a separate function. Listing 7.28 shows the extracted function. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 138 Objects and Prototypal Inheritance Listing 7.28 Implementing inherit if (!Function.prototype.inherit) { (function () { function F() {} Function.prototype.inherit = function (superFn) { F.prototype = superFn.prototype; this.prototype = new F(); this.prototype.constructor = this; }; }()); } This implementation uses the same intermediate constructor for all calls, only assigning the prototype for each call. Using this new function we can clean up our circles and spheres, as seen in Listing 7.29. Listing 7.29 Making Sphere inherit from Circle with inherit function Sphere (radius) { this.radius = radius; } Sphere.inherit(Circle); More or less allthe majorJavaScript libraries ship with a variant of theinherit function, usually under the name extend. I’ve named it inherit in order to avoid confusion when we turn our attention to another extend method later in this chapter. 7.3.2 Accessing [[Prototype]] The inherit function we just wrotemakes it possibleto easily create objecthierarchies using constructors. Still, comparing the Circle and Sphere constructors tells us something isn’t quite right—they both perform the same initialization of the radius property. The inheritance we’ve set up exists on the object level through the prototype chain, the constructors are not linked in the same way a class and a subclass are linked in a classical language. In particular, JavaScript has no super to directly refer to properties on objects from which an object inherits. In fact, ECMA-262 3rd edition provides no way at all to access the internal [[Prototype]] property of an object. Even though there is no standardized way of accessing the [[Prototype]] of an object, some implementations provide a non-standard __ proto __ property From the Library of WoweBook.Com Download from www.eBookTM.com ptg 7.3 Pseudo-classical Inheritance 139 that exposes the internal [[Prototype]] property. ECMAScript 5 (ES5) has standardized this feature as the new method Object.getPrototypeOf (object), giving you the ability to look up an object’s [[Prototype]]. In browsers in which __ proto __ is not available, we can sometimes use the constructor property to get the [[Prototype]], but it requires that the object was in fact created using a constructor and that the constructor property is set correctly. 7.3.3 Implementing super So, JavaScript has no super, and it is not possible to traverse the prototype chain in a standardized manner that is guaranteed to work reliably cross-browser. It’s still possible to emulate the concept of super in JavaScript. Listing 7.30 achieves this by calling the Circle constructor from within the Sphere constructor, passing the newly created object as the this value. Listing 7.30 Accessing the Circle constructor from within the Sphere constructor function Sphere(radius) { Circle.call(this, radius); } Sphere.inherit(Circle); Running the tests confirms that sphere objects still work as intended. We can employ the same technique to access “super methods” from other methods as well. In Listing 7.31 we call the area method on the prototype. Listing 7.31 Calling a method on the prototype chain Sphere.prototype.area = function () { return 4 * Circle.prototype.area.call(this); }; Listing 7.32 shows a simple test of the new method. Listing 7.32 Calculating surface area "test should calculate sphere area": function () { var sphere = new Sphere(3); assertEquals(113, Math.round(sphere.area())); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 140 Objects and Prototypal Inheritance The drawback of this solution is its verbosity; The call to Circle. prototype.area is long and couples Sphere very tightly to Circle. To miti- gate this, Listing 7.33 makes the inherit function set up a “super” link for us. Listing 7.33 Expecting the _ super link to refer to the prototype "test should set up link to super": function () { var SubFn = function () {}; var SuperFn = function () {}; SubFn.inherit(SuperFn); assertEquals(SuperFn.prototype, SubFn.prototype._super); } Note the leading underscore. ECMA-262 defines super as a reserved word intended for future use, so we best not use it. The implementation in Listing 7.34 is still straightforward. Listing 7.34 Implementing a link to the prototype if (!Function.prototype.inherit) { (function () { function F() {} Function.prototype.inherit = function (superFn) { F.prototype = superFn.prototype; this.prototype = new F(); this.prototype.constructor = this; this.prototype._super = superFn.prototype; }; }()); } Using this new property, Listing 7.35 simplifies Sphere.prototype.area. Listing 7.35 Calling a method on the prototype chain Sphere.prototype.area = function () { return 4 * this._super.area.call(this); }; 7.3.3.1 The _ super Method Although I would definitely not recommend it, someone serious about emulating classical inheritance in JavaScript would probably prefer _ super to be a method From the Library of WoweBook.Com Download from www.eBookTM.com ptg 7.3 Pseudo-classical Inheritance 141 rather than a simple link to the prototype. Calling the method should magically call the corresponding method on the prototype chain. The concept is illustrated in Listing 7.36. Listing 7.36 Testing the _ super method "test super should call method of same name on protoype": function () { function Person(name) { this.name = name; } Person.prototype = { constructor: Person, getName: function () { return this.name; }, speak: function () { return "Hello"; } }; function LoudPerson(name) { Person.call(this, name); } LoudPerson.inherit2(Person, { getName: function () { return this._super().toUpperCase(); }, speak: function () { return this._super() + "!!!"; } }); var np = new LoudPerson("Chris"); assertEquals("CHRIS", np.getName()); assertEquals("Hello!!!", np.speak()); } In this example we are using Function.prototype.inherit2 to estab- lish the prototype chain for the LoudPerson objects. It accepts a second argument, From the Library of WoweBook.Com Download from www.eBookTM.com ptg 142 Objects and Prototypal Inheritance which is an object that defines the methods on LoudPerson.prototype that need to call _ super. Listing 7.37 shows one possible implementation. Listing 7.37 Implementing _ super as a method if (!Function.prototype.inherit2) { (function () { function F() {} Function.prototype.inherit2 = function (superFn, methods) { F.prototype = superFn.prototype; this.prototype = new F(); this.prototype.constructor = this; var subProto = this.prototype; tddjs.each(methods, function (name, method) { // Wrap the original method subProto[name] = function () { var returnValue; var oldSuper = this._super; this._super = superFn.prototype[name]; try { returnValue = method.apply(this, arguments); } finally { this._super = oldSuper; } return returnValue; }; }); }; }()); } This implementation allows for calls to this. _ super() as if the method had special meaning. In reality, we’re wrapping the original methods in a new function that takes care of setting this. _ super to the right method before calling the original method. Using the new inherit function we could now implement Sphere as seen in Listing 7.38. From the Library of WoweBook.Com Download from www.eBookTM.com . linked in a classical language. In particular, JavaScript has no super to directly refer to properties on objects from which an object inherits. In fact, ECMA-262 3rd edition provides no way at. Inheritance When we assign a new object to Circle.prototype, JavaScript no longer creates a constructor property for us. This means that the [[Get]] for con- structor will go up the prototype chain until. Library of WoweBook.Com Download from www.eBookTM.com ptg 7.3 Pseudo-classical Inheritance 137 // Don't forget the constructor - else it will resolve as // Circle through the prototype chain Sphere.prototype.constructor

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