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

Test Driven JavaScript Development- P8 pps

20 284 0
Tài liệu đã được kiểm tra trùng lặp

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

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 20
Dung lượng 188,93 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

Listing 7.20 Broken constructor property "test constructor is Object when prototype is overridden": function { function Circle {} Circle.prototype = {}; assertEqualsObject, new Circle.c

Trang 1

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)

Trang 2

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-structorwill 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 propprop-erty 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));

Trang 3

By not overwriting the prototype property, we are also avoiding its constructorproperty 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;

}

Trang 4

Whenever someone forgets the new operator when calling the constructor, thiswill 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();

}());

Trang 5

// 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

Trang 6

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 all the major JavaScript libraries ship with a variant of the inherit 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 wrote makes it possible to easily create object hierarchies

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

Trang 7

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()));

}

Trang 8

The drawback of this solution is its verbosity; The call to Circle

prototype.areais 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_superlink 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_superMethod

Although I would definitely not recommend it, someone serious about emulating

classical inheritance in JavaScript would probably prefer_superto be a method

Trang 9

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_supermethod

"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,

Trang 10

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_superas 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

Trang 11

Listing 7.38 Implementing Sphere with inherit2

function Sphere(radius) { Circle.call(this, radius);

} Sphere.inherit2(Circle, { area: function () { return 4 * this._super();

} });

7.3.3.2 Performance of the super Method

Using the inherit2 method we can create constructors and objects that come pretty close to emulating classical inheritance It does not, however, per-form particularly well By redefining all the methods and wrapping them in closures, inherit2 will not only be slower than inherit when extend-ing constructors, but callextend-ing this._super() will be slower than calling this._super.method.call(this)as well

Further hits to performance are gained by the try-catch, which is used to ensure that this._super is restored after the method has executed As if that wasn’t enough, the method approach only allows static inheritance Adding new methods

to Circle.prototype will not automatically expose_superin same named methods on Sphere.prototype To get that working we would have to imple-ment some kind of helper function to add methods that would add the enclosing function that sets up_super In any case, the result would be less than elegant and would introduce a possibly significant performance overhead

I hope you never use this function; JavaScript has better patterns in store If anything, I think the_superimplementation is a testament to JavaScript’s flexi-bility JavaScript doesn’t have classes, but it gives you the tools you need to build them, should you need to do so

7.3.3.3 A_superHelper Function

A somewhat saner implementation, although not as concise, can be achieved by implementing _super as a helper function piggybacking the prototype link, as seen in Listing 7.39

Ngày đăng: 03/07/2014, 05:20

TỪ KHÓA LIÊN QUAN