Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ajax In Action (2006).pdf
Скачиваний:
63
Добавлен:
17.08.2013
Размер:
8.36 Mб
Скачать

Objects in JavaScript

601

 

 

Scope and inheritance can be useful features when defining a domain model. Unfortunately, JavaScript doesn’t support either natively. That hasn’t stopped people from trying, however, and some fairly elegant solutions have developed.

Doug Crockford (see the Resources section at the end of this appendix) has developed some ingenious workarounds that enable both inheritance and scope in JavaScript objects. What he has accomplished is undoubtedly impressive and, unfortunately, too involved to merit a detailed treatment here. The syntax that his techniques require can be somewhat impenetrable to the casual reader, and in a team-based project, adopting such techniques should be considered similar to adopting a Java framework of the size and complexity of Struts or Tapestry—that is, either everybody uses it or nobody does. I urge anyone with an interest in this area to read the essays on Crockford’s website.

Within the world of object orientation, there has been a gradual move away from complex use of inheritance and toward composition. With composition, common functionality is moved out into a helper class, which can be attached as a member of any class that needs it. In many scenarios, composition can provide similar benefits to inheritance, and JavaScript supports composition perfectly adequately.

The next stop in our brief tour of JavaScript objects is to look at reflection.

B.2.5 Reflecting on JavaScript objects

In the normal course of writing code, the programmer has a clear understanding of how the objects he is dealing with are composed, that is, what their properties and methods do. In some cases, though, we need to be able to deal with completely unknown objects and discover the nature of their properties and methods before dealing with them. For example, if we are writing a logging or debugging system, we may be required to handle arbitrary objects dumped on us from the outside world. This discovery process is known as reflection, and it should be familiar to most Java and .NET programmers.

If we want to find out whether a JavaScript object supports a certain property or method, we can simply test for it:

if (MyObject.someProperty){

...

}

This will fail, however, if MyObject.someProperty has been assigned the boolean value false, or a numerical 0, or the special value null. A more rigorous test would be to write

if (typeof(MyObject.someProperty) != "undefined"){

602APPENDIX B

JavaScript for object-oriented programmers

If we are concerned about the type of the property, we can also use the instanceof operator. This recognizes a few basic built-in types:

if (myObj instanceof Array){

...

}else if (myObj instanceof Object){

...

}

as well as any class definitions that we define ourselves through constructors:

if (myObj instanceof MyObject){

...

}

If you do like using instanceof to test for custom classes, be aware of a couple of “gotchas.” First, JSON doesn’t support it—anything created with JSON is either a JavaScript Object or an Array. Second, built-in objects do support inheritance among themselves. Function and Array, for example, both inherit from Object, so the order of testing matters. If we write

function testType(myObj){

if (myObj instanceof Array){ alert("it's an array");

}else if (myObj instanceof Object){ alert("it's an object");

}

}

testType([1,2,3,4]);

and pass an Array through the code, we will be told—correctly—that we have an Array. If, on the other hand, we write

function testType(myObj){

if (myObj instanceof Object){ alert("it's an object");

}else if (myObj instanceof Array){ alert("it's an array");

}

}

testType([1,2,3,4]);

then we will be told that we have an Object, which is also technically correct but probably not what we intended.

Finally, there are times when we may want to exhaustively discover all of an object’s properties and functions. We can do this using the simple for loop:

function MyObject(){ this.color='red'; this.flavor='strawberry';

Objects in JavaScript

603

 

 

this.azimuth='45 degrees'; this.favoriteDog='collie';

}

var myObj=new MyObject();

var debug="discovering...\n"; for (var i in myObj){

debug+=i+" -> "+myObj[i]+"\n";

}

alert(debug);

This loop will execute four times, returning all the values set in the constructor. The for loop syntax works on built-in objects, too—the simple debug loop above produces very big alert boxes when pointed at DOM nodes! A more developed version of this technique is used in the examples in chapters 5 and 6 to develop the recursive ObjectViewer user interface.

There is one more feature of the conventional object-oriented language that we need to address—the virtual class or interface. Let’s look at that now.

B.2.6 Interfaces and duck typing

There are many times in software development when we will want to specify how something behaves without providing a concrete implementation. In the case of our Shape object being subclassed by squares, circles, and so on, for example, we know that we will never hold a shape in our hands that is not a specific type of shape. The base concept of the Shape object is a convenient abstraction of common properties, without a real-world equivalent.

A C++ virtual class or a Java interface provides us with the necessary mechanism to define these concepts in code. We often speak of the interface defining a contract between the various components of the software. With the contract in place, the author of a Shape-processing library doesn’t need to consider the specific implementations, and the author of a new implementation of Shape doesn’t need to consider the internals of any library code or any other existing implementations of the interface.

Interfaces provide good separation of concerns and underpin many design patterns. If we’re using design patterns in Ajax, we want to use interfaces. JavaScript has no formal concept of an interface, so how do we do it?

The simplest approach is to define the contract informally and simply rely on the developers at each side of the interface to know what they are doing. Dave Thomas has given this approach the engaging name of “duck typing”—if it walks like a duck and it quacks like a duck, then it is a duck. Similarly with our Shape interface, if it can compute an area and a perimeter, then it is a shape.

604APPENDIX B

JavaScript for object-oriented programmers

Let’s suppose that we want to add the area of two shapes together. In Java, we could write

public double addAreas(Shape s1, Shape s2){ return s1.getArea()+s2.getArea();

}

The method signature specifically forbids us from passing in anything other than a shape, so inside the method body, we know we’re following the contract. In JavaScript, our method arguments aren’t typed, so we have no such guarantees:

function addAreas(s1,s2){

return s1.getArea()+s2.getArea();

}

If either object doesn’t have a function getArea() attached to it, then we will get a JavaScript error. We can check for the presence of the function before we call it:

function hasArea(obj){

return obj && obj.getArea && obj.getArea instanceof Function;

}

and modify our function to make use of the check:

function addAreas(s1,s2){ var total=null;

if (hasArea(s1) && hasArea(s2)){ total=s1.getArea()+s2.getArea();

}

return total;

}

Using JavaScript reflection, in fact, we can write a generic function to check that an object has a function of a specific name:

function implements(obj,funcName){

return obj && obj[funcName] && obj[funcName] instanceof Function;

}

Or, we can attach it to the prototype for the Object class:

Object.prototype.implements=function(funcName){

return this && this[funcName] && this[funcName] instanceof Function;

}

That allows us to check specific functions by name:

function hasArea(obj){

return obj.implements("getArea");

}

Objects in JavaScript

605

 

 

or even to test for compliance with an entire interface:

function isShape(obj){

return obj.implements("getArea") && obj.implements("getPerimeter");

}

This gives us some degree of safety, although still not as much as we would get under Java. A rogue object might implement getArea() to return a string rather than a numerical value, for example. We have no way of knowing the return type of a JavaScript function unless we call it, because JavaScript functions have no predefined type. (Indeed, we could write a function that returns a number during the week and a string on weekends.) Writing a set of simple test functions to check return types is easy enough; for example:

function isNum(arg){

return parseFloat(arg)!=NaN;

}

NaN is short for “not a number,” a special JavaScript variable for handling number format errors. This function will actually return true for strings if they begin with a numeric portion, in fact. parseFloat() and its cousin parseInt() will try their best to extract a recognizable number where they can. parseFloat("64 hectares") will evaluate to 64, not NaN.

We could firm up our addAreas() function a little further:

function addAreas(s1,s2){ var total=null;

if (hasArea(s1) && hasArea(s2)){ var a1=s1.getArea();

var a2=s2.getArea();

if (isNum(a1) && isNum(a2)){ total=parseFloat(a1)+parseFloat(a2);

}

}

return total;

}

I call parseFloat() on both arguments to correctly handle strings that slip through the net. If s1 returns a value of 32 and s2 a value of 64 hectares, then addAreas() will return 96. If I didn’t use parseFloat, I’d get a misleading value of

3264 hectares!

In summary, duck typing keeps things simple but requires you to trust your development team to keep track of all the details. Duck typing is popular among the Ruby community, who are generally a very smart bunch. As one moves from a single author or small tightly bound team to larger projects involving separate