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

606APPENDIX B

JavaScript for object-oriented programmers

subgroups, this trust inevitably weakens. If you want to add a few checks and balances to your code on top of duck typing, then perhaps this section will have shown you where to start.

We’ve looked at the language from the point of view of objects. Now let’s drill down a little to look at these functions that we’ve been throwing around and see what they really are.

B.3 Methods and functions

We’ve been defining functions and calling them in the previous section and in the rest of this book. A Java or C# programmer might have assumed that they were something like a method, defined with a slightly funny-looking syntax. In this section, we’ll take functions apart a bit more and see what we can do with them.

B.3.1 Functions as first-class citizens

Functions are a bit like Java methods in that they have arguments and return values when invoked, but there is a key difference. A Java method is inherently bound to the class that defined it and cannot exist apart from that class. A JavaScript function is a free-floating entity, a first-class object in its own right. (Static Java methods lie somewhere in between these two—they are not bound to any instance of an object but are still attached to a class definition.)

Programmers who have worked their way through the C family may think “Ah, so it’s like a function pointer in C++, then.” It is indeed, but that’s not the end of it.

In JavaScript, Function is a type of built-in object. As expected, it contains executable code, and can be invoked, but it is also a descendant of Object, and can do everything that a JavaScript object can, such as storing properties by name. It is quite possible (and quite common) for a Function object to have other Function objects attached to it as methods.

We’ve already seen how to get a reference to a Function object. More usually, we would want to reference a function and invoke it in a single line, such as

var result=MyObject.doSomething(x,y,z)

However, the Function is a first-class object, and it can also be executed via the call() method (and its close cousin apply()):

var result=MyObject.doSomething.call(MyOtherObject,x,y,z)

or even

Methods and functions

607

 

 

var result=MyObject['doSomething'].call(MyOtherObject,x,y,z)

The first argument of Function.call() is the object that will serve as the function context during the invocation, and subsequent arguments are treated as arguments to the function call. apply() works slightly differently in that the second argument is an array of arguments to pass to the function call, allowing greater flexibility in programmatically calling functions whose argument list length is undetermined.

It’s worth pointing out here that the argument list to a JavaScript function is not of a fixed length. Calling a Java or C# method with more or fewer arguments than it declares would generate a compile-time error. JavaScript just ignores any extra args and assigns undefined to missing ones. A particularly clever function might query its own argument list through the arguments property and assign sensible defaults to missing values, throw an exception, or take any other remedial action. This can be exploited to combine a getter and setter method into a single function, for example:

function area(value){ if (value){

this.area=value;

}

return this.area;

}

If we simply call area(), then value is undefined, so no assignment takes place, and our function acts as a getter. If a value is passed in, our function acts as a setter. This technique is used extensively by Mike Foster’s x library (see the Resources section at the end of this chapter, and also chapter 3), so if you plan on working with that, you’ll soon be familiar with the idiom.

Functions become really interesting, though, when we take advantage of their independence as first-class objects.

B.3.2 Attaching functions to objects

As a functional language, JavaScript allows us to define functions in the absence of any object, for example:

function doSomething(x,y,z){ ... }

Functions can also be defined inline:

var doSomething=function(x,y,z){ ... }

As a concession to object-orientation, functions may be attached to objects, giving the semblance of a Java or C# method. There is more than one way of doing this.

608APPENDIX B

JavaScript for object-oriented programmers

We can attach a predefined function to a predefined object (in which case only that object can call the function, not any other object derived from the same prototype):

myObj.doSomethingNew=doSomething;

myObj.doSomethingNew(x,y,z);

We can also attach functions such that every instance of a class can access them, by adding the function (either predefined or declared inline) to the new object in the constructor, as we saw in section B.2.2, or by attaching it to the prototype.

Once we’ve done this, though, they aren’t very strongly attached, as we will see.

B.3.3 Borrowing functions from other objects

Giving functions first-class object status alters the capabilities of a language significantly. Furthermore, an understanding of these alterations is important when coding GUI event handling, so most Ajax programmers would do well to understand it.

So what are these new capabilities? Well, first off, one object can borrow another’s function and call it on itself. Let’s define a class to represent species of tree in a taxonomic system:

function Tree(name, leaf, bark){ this.name=name; this.leaf=leaf; this.bark=bark;

}

Next, we’ll add a function to it that provides a quick description of the tree:

Tree.prototype.describe=function(){

return this.name+": leaf="+this.leaf+", bark="+this.bark;

}

If we now instantiate a Tree object and ask it to describe itself, we get a fairly predictable response:

var Beech=new Tree("green, serrated edge","smooth"); alert(Beech.describe());

The alert box will display the text Beech: leaf=green, serrated edge, bark=smooth. So far, so good. Now let us define a class to represent a dog:

function Dog(name,bark){ this.name=name; this.bark=bark;

}

HTML markup; for

Methods and functions

609

 

 

and create an instance of our Dog class:

var Snowy=new Dog("snowy","wau! wau!");

Snowy wants to show us his bark, but, although we’ve defined it for him, he has no function through which to express it. He can, however, hijack the Tree class’s function:

var tmpFunc=Beech.describe; tmpFunc.call(Snowy);

Remember, the first argument to function.call() is the context object, that is, the object that the special variable this will resolve to. The previous code will generate an alert box displaying the text Snowy: leaf=undefined, bark=wau! wau!. Well, it’s better than nothing for the poor dog.

So what’s going on here? How can a dog call a function that really belongs to a tree? The answer is that the function doesn’t belong to the tree. Despite being peppered with references to this, assigning the function to the Tree prototype binds it only inasmuch as it enables us to use the shorter MyTree.describe() notation. Internally, the function is stored as a piece of text that gets evaluated every time it is called, and that allows the meaning of this to differ from one invocation to the next.

Borrowing functions is a neat trick that we can use in our own code, but in production-quality code, we’d prefer to see someone implement a bark() method for Snowy of his very own. The real reason for discussing this behavior is that when you are writing event-handling code, the web browser will do it for you behind the scenes.

B.3.4 Ajax event handling and function contexts

Ajax event handlers are pretty much the same as most GUI toolkit languages, with specialized categories for mouse and keyboard events, as we saw in chapter 4. Our example uses the onclick handler, fired when a mouse is clicked on a visible element. A full discussion of DHTML event handling is beyond the scope of this book, but let’s take the time here to highlight a particular issue that can often trip up the unwary.

Event handlers can be declared either as part of the example

<div id='myDiv' onclick='alert:alert(this.id)'></div>

or programmatically; for example:

610APPENDIX B

JavaScript for object-oriented programmers

function clickHandler(){ alert(this.id); } myDiv.onclick=clickHandler;

Note that in the programmatic case, we pass a reference to the Function object (that is no () after the clickHandler). When declaring the function in the HTML, we are effectively declaring an anonymous function inline, equivalent to

myDiv.onclick=function(){ alert(this.id); }

Note that in both cases, the function has no arguments assigned to it, nor is there any way for us to pass in arguments with the mouse click. However, when we click on the DOM element, the function is called with an Event object as the argument and the element itself as the context object. Knowing this can save a lot of grief and puzzlement, particularly when you’re writing object-based code. The key source of confusion is that the DOM node is always passed as context, even if the function is attached to the prototype of a different object.

In the following example, we define a simple object with an event handler for a visible GUI element that it knows about. We can think of the object as the Model in MVC terms, with the event handler taking the role of Controller, and the DOM element being the View.

function myObj(id,div){ this.id=id; this.div=div;

this.div.onclick=this.clickHandler;

}

The constructor takes an internal ID and a DOM element, to which it assigns an onclick handler. We define the event handler as follows:

myObj.prototype.clickHandler=function(event){

alert(this.id);

}

So, when we click on the GUI element, it will alert us to the ID of that object, right? In fact, it won’t, because the myObj.clickHandler function will get borrowed by the browser (just as our wayward dog borrowed a method from the tree object in the previous section) and invoked in the context of the element, not the Model object. Since the element happens to have a built-in id property, it will show a value, and, depending on your naming conventions, it may even be the same as the Model object’s ID, allowing the misunderstanding to continue for some time.

If we want the event handler to refer to the Model object that we’ve attached it to, we need another way of passing the reference to that object across. There

Methods and functions

611

 

 

are two idioms for doing this that I’ve commonly come across. One is clearly superior to the other, in my opinion, but I coded for years using the other one, and it works. One of the aims of this book is to give names to the patterns (and anti-patterns) that we have adopted by habit, so I will present both here.

Referencing the Model by name

In this solution, we assign a globally unique ID to each instance of our Model object and keep a global array of these objects referenced by the ID. Given a reference to a DOM element, we can then reference its Model object by using part of the ID as a key to the lookup array. Figure B.1 illustrates this strategy.

Generating a unique ID for every element is an overhead in this approach, but ID generation can be accomplished fairly automatically. We can use the array length as part of the key, for example, or a database key, if we’re generating code on the web server.

As a simple example, we’re creating an object of type myObj, which has a clickable title bar that invokes a function myObj.foo().

Here is the global array:

var MyObjects=new Array();

And here is the constructor function, which registers the Model object with that array:

Model object

DOM element

3. Retrieve reference

to Model

 

 

2. Query lookup

 

with id fragment

Global lookup

1. Resolve this.id and

extract fragment

 

 

Event handler

Figure B.1 Referencing the Model from an event handler function by name. The DOM element ID is parsed, and the parsed value used as a key to a global lookup array.

612APPENDIX B

JavaScript for object-oriented programmers

function myObj(id){ this.uid=id; MyObjects[this.uid]=this;

...

this.render();

}

Here is a method of the myObj object, which does something exciting. We want to invoke this when the title bar is clicked:

myObj.prototype.foo=function(){ alert('foooo!!! '+this.uid);

}

Here is the object’s render() method, which creates various DOM nodes:

myObj.prototype.render=function(){

...

this.body=document.createElement("div"); this.body.id=this.uid+"_body";

...

this.titleBar=document.createElement("div"); this.titleBar.id=this.uid+"_titleBar"; this.titleBar.onclick=fooEventHandler;

...

}

When we construct any DOM nodes in the view for this Model object, we assign an ID value to them that contains the Model object ID.

Note that we refer to a function fooEventHandler() and set it as the onclick property of our title bar DOM element:

function fooEventHandler(event){ var modelObj=getMyObj(this.id);

if (modelObj){ modelObj.foo(); }

}

}

The event handler function will need to find the instance of myObj in order to invoke its foo() method. We provide a finder method:

function getMyObj(id){

var key=id.split("_")[0]; return MyObjects[key];

}

It has a reference to the DOM node and can use its id property to extract a key from which to retrieve the Model object from the global array.

And there it is. The Reference Model By Name method served me well for a few years, and it works, but there is a simpler, cleaner way that doesn’t pepper your

Methods and functions

613

 

 

DOM tree with lengthy IDs. (Actually, I never reached a decision as to whether that was good or bad. It was a waste of memory, for sure, but it also made debugging in the Mozilla DOM Inspector very easy.)

Attaching a Model to the DOM node

In this second approach to DOM event handling, everything is done with object references, not strings, and no global lookup array is needed. This is the approach that has been used throughout this book. Figure B.2 illustrates this approach.

This approach simplifies the event handler’s job considerably. The constructor function for the Model object needs no specialized ID manipulation, and the foo() method is defined as before. When we construct DOM nodes, we exploit JavaScript’s dynamic ability to attach arbitrary attributes to any object and clip the Model object directly onto the DOM node receiving the event:

myObj.prototype.createView=function(){

...

this.body=document.createElement("div");

this.body.modelObj=this;

...

this.titleBar=document.createElement("div");

this.titleBar.modelObj=this;

this.titleBar.onclick=fooEventHandler;

...

}

When we write the event handler, we can then get a direct reference back to the Model:

Model object

DOM element

2.Resolve this.modelObj

1. Resolve this

Event handler

Figure B.2

Attaching a reference to the Model directly to a DOM node makes it easier for the event handler function to find the Model at runtime.