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

614APPENDIX B

JavaScript for object-oriented programmers

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

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

}

}

No finders, no global lookups—it’s as simple as that.

One word of warning, however. When using this pattern, we create a cyclic reference between a DOM and a non-DOM variable, and web browser folklore has it that this is bad for garbage collection under certain popular browsers of the day. If this pattern is used correctly, memory overheads can be avoided, but I’d recommend you study chapter 7 before implementing the Attach Model To DOM Node pattern.

Understanding how a JavaScript function has defined its context has helped us to develop an elegant reusable solution for the browser event model, then. The ability of a function to switch between contexts can be confusing at first, but understanding the model behind us helps to work with it.

The final thing that we need to understand about JavaScript functions is the language’s ability to create closures. Again, Java and C# lack the concept of closures, although some Java and .NET scripting languages, such as Groovy and Boo, support them, and C# 2.0 will support them, too. Let’s look at what they are and how to work with them.

B.3.5 Closures in JavaScript

On its own, a Function object is incomplete—to invoke it, we need to pass in a context object and a set of arguments (possibly an empty set). At its simplest, a closure can be thought of as a Function bundled with all the resources that it needs to execute.

Closures are created in JavaScript implicitly, rather than explicitly. There is no constructor function new Closure() and no way to get a handle on a closure object. Creating a closure is as simple as declaring a function within a code block (such as another function) and making that function available outside the block.

Again, this sounds a bit weird conceptually but is simple enough when we look at an example. Let’s define a simple object to represent a robot and record the system clock time at which each robot is created. We can write a constructor like this:

function Robot(){

var createTime=new Date(); this.getAge=function(){

var now=new Date();

Methods and functions

615

 

 

var age=now-createTime; return age;

}

}

(All the robots are identical, so we haven’t bothered to assign names or anything else through constructor arguments.) Normally, we would record createTime as a member property, that is, write

this.createTime=new Date();

but here we’ve deliberately created it as a local variable, whose scope is limited to the block in which it is called, that is, the constructor. On the second line of the constructor, we define a function getAge(). Note here that we’re defining a function inside another function and that the inner function uses the local variable createTime, belonging to the scope of the outer function. By doing this, and nothing else, we have in fact created a closure. If we define a robot and ask it how old it is once the page has loaded,

var robbie=new Robot();

window.onload=function(){

alert(robbie.getAge());

}

then it works and gives us a value of around 10–50 milliseconds, the difference between the script first executing and the page loading up. Although we have declared createTime as being local to the constructor function scope, it cannot be garbage-collected so long as Robbie the robot is still referenced, because it has been bound up in a closure.

The closure works only if the inner function is created inside the outer one. If we refactor my code to predefine the getAge function and share it between all robot instances, like so

function Robot(){

var createTime=new Date(); this.getAge=roboAge;

}

function roboAge(){ var now=new Date();

var age=now-createTime; return age;

};

then the closure isn’t created, and we get an error message telling me that createTime is not defined.

616APPENDIX B

JavaScript for object-oriented programmers

Closures are very easy to create and far too easy to create accidentally, because closures bind otherwise local variables and keep them from the garbage collector. If DOM nodes, for example, get caught up in this way, then inadvertently created closures can lead to significant memory leaks over time.

The most common situation in which to create closures is when binding an event-handler callback function to the source of the event. As we discussed in section B.3.4, the callback is invoked with a context and set of arguments that is sometimes not as useful as it might be. We presented a pattern for attaching additional references (the Model object) to the DOM element that generates the event, allowing us to retrieve the Model via the DOM element. Closures provide an alternative way of doing this, as illustrated here:

myObj.prototype.createView=function(){

...

this.titleBar=document.createElement("div"); var modelObj=this; this.titleBar.onclick=function(){

fooEventHandler.call(modelObj);

}

}

The anonymous onclick handler function that we define makes a reference to the locally declared variable modelObj, and so a closure is created around it, allowing modelObj to be resolved when the function is invoked. Note that closures will resolve only local variables, not those referenced through this.

We use this approach in the ContentLoader object that we introduced in chapter 2, because the onreadystatechange callback provided in Internet Explorer returns the window object as the function context. Since window is defined globally, we have no way of knowing which ContentLoader’s readyState has changed, unless we pass a reference to the relevant loader object through a closure.

My recommendation to the average Ajax programmer is to avoid closures if there is an alternative. If you use the prototype to assign functions to your custom object types, then you don’t duplicate the functions and you don’t create closures. Let’s rewrite our Robot class to follow this advice:

function Robot(){ this.createTime=new Date();

}

Robot.prototype.getAge=function(){ var now=new Date();

var age=now-this.createTime; return age;

};