home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam    

Book HomeActionScript: The Definitive GuideSearch this book

12.5. Classes and Object-Oriented Programming

It's not uncommon to create dozens of complex objects that store rich information about everything from products in a shopping-cart system to bad guys with artificial intelligence in a video game. To expedite the creation of objects and to define object hierarchies (relationships between objects), we use object classes. A class is a template-style definition of an entire category of objects. As we learned in the introduction, classes describe the general features of a specific breed of objects, such as "all dogs have four legs."

12.5.1. Object Classes

Before we see how to use classes, let's see how things work when we don't use them. Suppose we want a ball object, but instead of using a class to generate it, we simply adapt a generic object of the built-in Object class. We give the object these properties: radius, color, xPosition, and yPosition. Then we add two methods -- moveTo( ) and area( ) -- used to reposition the object and to determine the amount of space it occupies.

Here's the code:

var ball = new Object( );
ball.radius = 10;
ball.color = 0xFF0000;
ball.xPosition = 59;
ball.yPosition = 15;
ball.moveTo = function (x, y) { this.xPosition = x; this.yPosition = y; };
ball.area = function ( ) { return Math.PI * (this.radius * this.radius); };

That approach gets the job done but has limitations; every time we want a new ball-like object, we have to repeat the ball-initializing code, which is tedious and error-prone. In addition, creating many ball objects in this way redundantly duplicates the identical moveTo( ) and area( ) function code on each object, unnecessarily taking up memory.

To efficiently create a series of objects that have common properties, we should create a class. Using a class, we can define the properties that all ball objects should possess. Furthermore, we can share any properties with fixed values across all instances of the ball class. In natural language our Ball class would be described to the interpreter as follows:

A Ball is a type of object. All ball object instances have the properties radius, color, xPosition, yPosition, which are set individually for each ball. All ball objects also share the methods moveTo( ) and area( ), which are identical across all members of the Ball class.

Let's see how this theory works in practice.

12.5.2. Making a Class

There is no specific "class declaration" device in ActionScript; there is no "class" statement that creates a new class akin to the var statement, which creates new variables. Instead, we define a special type of function, called a constructor function, that will generate a new instance of our class. By defining the constructor function, we are effectively creating our class template or class definition.

Syntactically, constructor functions (or simply, constructors) are formed just like normal functions. For example:

function Constructor ( ) { 
  statements
}

The name of a class's constructor function may be any valid function name, but it is capitalized by convention to indicate that it is a class constructor. A constructor's name should describe the class of objects it creates, as in Ball, Product, or Vector2d. The statements of a constructor function initialize the objects it creates.

We'll make our Ball constructor function as simple as possible to start. All we want it to do so far is create empty objects for us:

// Make a Ball constructor
function Ball ( ) {
  // Do something here
}

That didn't hurt much. Now let's see how to generate ball objects using our Ball constructor function.

12.5.2.2. Assigning custom properties to the objects of a class

To customize an object's properties during the object-creation stage, we again turn to the special this keyword. Within a constructor function, the this keyword stores a reference to the object currently being generated. Using that reference, we can assign whatever properties we like to the embryonic object. The following syntax shows the general technique:

function Constructor ( ) {
  this.propertyName = value;
}

where this is the object being created, propertyName is the property we want to attach to that object, and value is the data value we're assigning to that property.

Let's apply this technique to our Ball example. Earlier, we proposed that the properties of the Ball class should be radius, color, xPosition, and yPosition. Here's how the Ball class constructor might assign those properties to its instances (note the use of the this keyword):

function Ball ( ) {
  this.radius = 10;
  this.color = 0xFF0000;
  this.xPosition = 59;
  this.yPosition = 15;
}

With the Ball constructor thus prepared, we can create object instances (i.e., members of the class) bearing the predefined properties -- radius, color, xPosition, and yPostion -- by invoking Ball( ) with the new operator, just as we did earlier. For example:

// Make a new instance of Ball
bouncyBall = new Ball( );

// Now access the properties of bouncyBall that were set when it was
// made by the Ball( ) constructor.
trace(bouncyBall.radius);     // Displays: 10
trace(bouncyBall.color);      // Displays: 16711680
trace(bouncyBall.xPosition);  // Displays: 59
trace(bouncyBall.yPosition);  // Displays: 15

Now wasn't that fun?

Unfortunately, our Ball( ) constructor uses fixed values when it assigns properties to the objects it creates (e.g., this.radius = 10). Every object of the Ball class, therefore, would have the same property values, which is antithetical to the goal of object-oriented programming (we have no need for a class that generates a bunch of identical objects; it is their differences that make them interesting).

To dynamically assign property values to instances of a class, we adjust our constructor function so that it accepts arguments. Let's consider the general syntax, then get back to the Ball example:

function Constructor (value1, value2, value3) {
  this.property1 = value1;
  this.property2 = value2;
  this.property3 = value3;
}

Inside the Constructor function, we refer to the object being created with the this keyword, just as we did earlier. This time, however, we don't hardcode the object properties' values. Instead, we assign the values of the arguments value1, value2, and value3 to the object's properties. When we want to create a new, unique member of our class, we pass our class constructor function the initial property values for our new instance:

myObject = new Constructor (value1, value2, value3);

Let's see how this applies to our Ball class, shown in Example 12-5.

Example 12-5. A Generalized Ball Class

// Make the Ball( ) constructor accept property values as arguments
function Ball (radius, color, xPosition, yPosition) {
  this.radius = radius;
  this.color = color;
  this.xPosition = xPosition;
  this.yPosition = yPosition;
}

// Invoke our constructor, passing it arguments to use as
// our object's property values
myBall = new Ball(10, 0x00FF00, 59, 15);

// Now let's see if it worked...
trace(myBall.radius);  // Displays: 10,  :) Pretty cool...

We're almost done building our Ball class. But you'll notice that we're still missing the moveTo( ) and area( ) methods discussed earlier. There are actually two ways to attach methods to a class's objects. We'll learn the simple, less efficient way now and come back to the topic of method creation later when we cover inheritance.

12.5.2.3. Assigning methods to objects of a class

The simplest way to add a method to a class is to assign a property that contains a function within the constructor. Here's the generic syntax:

function Constructor( ) {
  this.methodName = function;
}

The function may be supplied in several ways, which we'll examine by adding the area( ) method to our Ball class. Note that we've removed the color, xPosition, and yPosition properties from the class for the sake of clarity.

The function can be a function literal, as in:

function Ball (radius) {
  this.radius = radius;
  // Add the area method...
  this.area = function ( ) { return Math.PI * this.radius * this.radius; };
}

Alternatively, the function can be declared inside the constructor:

function Ball (radius) {
  this.radius = radius;
  // Add the area method...
  this.area = getArea;
  function getArea ( ) {
    return Math.PI * this.radius * this.radius; 
  }
}

Finally, the function can be declared outside the constructor but assigned inside the constructor:

// Declare the getArea( ) function
function getArea ( ) {
  return Math.PI * this.radius * this.radius; 
}

function Ball (radius) {
  this.radius = radius;
  // Add the area method...
  this.area = getArea;
}

There's no real difference between the three approaches -- all are perfectly valid. Function literals often prove the most convenient to use but are perhaps not as reusable as defining a function outside of the constructor so that it can also be used in other constructors. Regardless, all three approaches lack efficiency.

So far we've been attaching unique property values to each object of our Ball class. Each ball object needs its own radius and color property values to distinguish one ball from the next. But when we assign a fixed method to the objects of a class using the techniques we've just seen, we are unnecessarily duplicating the method on every object of our class. The area formula is the same for every ball, and hence the code that performs this task should be centralized and generalized.

To more efficiently attach any property or method with a fixed value to a class, we use inheritance, our next topic of study.

12.5.3. Object Property Inheritance

Inherited properties are not attached to the individual object instances of a class. They are attached once to the class constructor and borrowed by the objects as necessary. These properties are said to be inherited because they are passed down to objects instead of being defined within each object. An inherited property appears to belong to the object through which it is referenced but is actually part of the object's class constructor.

Figure 12-2 demonstrates the general model for inherited properties using our Ball class as an example. Because the moveTo( ) and area( ) methods of Ball do not vary among its instances, those methods are best implemented as inherited methods. They belong to the class itself, and each ball object accesses them only by reference. On the other hand, the radius, color, xPosition, and yPosition properties of our Ball class are assigned to each object as normal properties because each ball needs its own value for those properties.

Figure 12-2

Figure 12-2. Inherited and normal properties

Inheritance works in a hierarchical chain, like a family tree. When we invoke a method of an individual object, the interpreter checks to see if that object implements the method. If the method isn't found, the interpreter then looks at the class for the method.

For example, if we execute ball1.area( ), the interpreter checks whether ball1 defines an area( ) method. If the ball1 object lacks an area( ) method, the interpreter checks whether the Ball class defines an area( ) method. If it does, the interpreter then invokes area( ) as though it were a method of the ball1 object, not the Ball class. This allows the method to operate on the ball1 object (rather than the class), retrieving or setting ball1's properties as necessary. This is one of the key benefits of OOP; we define the function in one place (the Ball class) but use it from many places (any ball object).

Unlike normal properties, inherited properties may only be retrieved through an object, not set.

Time for a little code to breathe some life into these principles.

12.5.3.1. Creating inherited properties with the prototype property

We start the process of creating inherited properties by creating a class constructor function, such as that of our Ball class in Example 12-5:

function Ball (radius, color, xPosition, yPosition) {
  this.radius = radius;
  this.color = color;
  this.xPosition = xPosition;
  this.yPosition = yPosition;
}

Remember from Chapter 9, "Functions", that functions double as objects and can therefore take properties.

TIP

When a constructor function is created, the interpreter automatically assigns it a property called prototype. In the prototype property, the interpreter places a generic object. Any properties attached to the prototype object are inherited by all instances of the constructor function's class.

To create a property that will be inherited by all the objects of a class, we simply assign that property to the prefabricated prototype object of the class's constructor function. Here's the general syntax:

Constructor.prototype.propName = value;

where Constructor is our class's constructor function (Ball in our example); prototype is the automatically generated property we use to house inherited properties; propName is the inherited property name, and value is that inherited property's value. For example, here's how we would add a global gravity property to our entire class of Ball objects:

Ball.prototype.gravity = 9.8;

With the gravity property in place, we can then access gravity from any member of the Ball class:

// Create a new instance of the Ball class
myBall = new Ball(5, 0x003300, 34, 220);

// Now display the value of the inherited gravity property
trace(myBall.gravity);  // Displays: 9.8

The gravity property is accessible through myBall because myBall inherits the properties of Ball 's prototype object.

Because the same methods are ordinarily shared by every instance of a class, they are typically stored in inherited properties. Let's add an inherited area( ) method to our Ball class:

Ball.prototype.area = function ( ) {
  return Math.PI * this.radius * this.radius; 
};  // Semicolon required because this is a function literal

That was so much fun, let's add an inherited moveTo( ) method, this time using a predefined function instead of a function literal:

function moveTo (x, y) {
  this.xPosition = x;
  this.yPosition = y;
}

Ball.prototype.moveTo = moveTo;

Once a function is defined as an inherited property, we can invoke it like any other method:

// Make a new ball
myBall = new Ball(15, 0x33FFCC, 100, 50);

// Now invoke myBall's inherited area( ) method
trace(myBall.area( ));  // Displays: 706.858347057703

Note that it's also possible to replace a constructor's prototype object entirely with a new object, thereby adding many inherited properties in a single gesture. Doing so, however, alters the inheritance chain, which we'll learn about later under "Superclasses and Subclasses."

12.5.4. Superclasses and Subclasses

One of the crucial features of classes in advanced object-oriented programming is their ability to share properties. That is, an entire class may inherit properties from, and pass properties on to, other classes. In complex situations, multiclass inheritance can be indispensable. (Even if you don't use multiclass inheritance yourself, understanding it will help you work with the built-in ActionScript classes.)

We've seen how objects inherit properties from the prototype object of their class constructors. Inheritance is not limited to that single object/class relationship. A class itself may inherit properties from other classes. For example, we may have a class called Circle that defines a general method, area( ), used to find the area of all circular objects. Instead of defining a separate area( ) method in a class like Ball, we may simply make Ball inherit the area( ) method already available from Circle. Instances of Ball, hence, inherit the Circle's area( ) method through Ball. Notice the hierarchy -- the simplest class, Circle, defines the most general methods and properties. Another class, Ball, builds on that simple Circle class, adding features specific to Ball-class instances but relying on Circle for the basic attributes that all circular objects share. In traditional object-oriented programming, the Ball class would be said to extend the Circle class. That is, Ball is a subclass of Circle, while Circle is a superclass of Ball.

12.5.4.1. Making a superclass

Earlier we learned to define inherited properties on the prototype object of a class's constructor function. To create a superclass for a given class, we completely replace the class's prototype object with a new instance of the desired superclass. Here's the general syntax:

Constructor.prototype = new SuperClass( );

By replacing Constructor's prototype object with an instance of SuperClass, we force all instances of Constructor to inherit the properties defined on instances of SuperClass. In Example 12-7, we first create a class, Circle, which assigns an area( ) method to all its instances. Then we assign an instance of Circle to Ball.prototype, causing all ball objects to inherit area( ) from Circle.

Example 12-7. Creating a Superclass

// Create a Circle (superclass) constructor
function Circle( ) {
  this.area = function ( ) { return Math.PI * this.radius * this.radius; };
}

// Create our usual Ball class constructor
function Ball ( radius, color, xPosition, yPosition ) {
  this.radius = radius;
  this.color = color;
  this.xPosition = xPosition;
  this.yPosition = yPosition;
}

// Here we make the superclass by assigning an instance of Circle to
// the Ball class constructor's prototype
Ball.prototype = new Circle( );

// Now let's make an instance of Ball and check its properties
myBall = new Ball ( 16, 0x445599, 34, 5);
trace(myBall.xPosition);   // 34, a normal property of Ball
trace(myBall.area( ));      // 804.24..., area( ) was inherited from Circle

However, our class hierarchy now has poor structure -- Ball defines radius, but radius is actually a property common to all circles, so it belongs in our Circle class. The same is true of xPosition and yPosition. To fix the structure, we'll move radius, xPosition, and yPosition to Circle, leaving only color in Ball. (For the sake of the example, we'll treat color as a property only balls can have.)

Conceptually, here's the setup of our revised Circle and Ball constructors:

// Create the Circle (superclass) constructor
function Circle ( radius, xPosition, yPosition ) {
  this.area = function ( ) { return Math.PI * this.radius * this.radius; };
  this.radius = radius;
  this.xPosition = xPosition;
  this.yPosition = yPosition;
}

// Create the Ball class constructor
function Ball ( color ) {
  this.color = color;
}

Having moved the properties around, we're faced with a new problem. How can we provide values for radius, xPosition, and yPosition when we're creating objects using Ball and not Circle ? We have to make one more adjustment to our Ball constructor code. First, we set up Ball to receive all the required properties as parameters:

function Ball ( color, radius, xPosition, yPosition ) {

Next, within our Ball constructor, we define the Circle constructor as a method of the ball object being instantiated:

this.superClass = Circle;

Finally, we invoke the Circle constructor on the ball object, and pass it values for radius, xPosition, and yPosition:

this.superClass(radius, xPosition, yPosition);

Our completed class/superclass code is shown in Example 12-8.

Example 12-8. A Class and Its Superclass

// Create a Circle (superclass) constructor
function Circle ( radius, xPosition, yPosition) {
  this.area = function ( ) { return Math.PI * this.radius * this.radius; };
  this.radius = radius;
  this.xPosition = xPosition;
  this.yPosition = yPosition;
}

// Create our Ball class constructor
function Ball ( color, radius, xPosition, yPosition ) {
  // Define the Circle superclass as a method of the ball being instantiated
  this.superClass = Circle;
  // Invoke the Circle constructor on the ball object, passing values
  // supplied as arguments to the Ball constructor
  this.superClass(radius, xPosition, yPosition);
  // Set the color of the ball object
  this.color = color;
}

// Assign an instance of our Circle superclass to our 
// Ball class constructor's prototype
Ball.prototype = new Circle( );

// Now let's make an instance of Ball and check its properties
myBall = new Ball ( 0x445599, 16, 34, 5);
trace(myBall.xPosition);   // 34
trace(myBall.area( ));      // 804.24...
trace(myBall.color);       // 447836

Note that the word superClass in Ball is not reserved or special. It's simply an apt name for the superclass constructor function. Furthermore, Circle 's area( ) method could have been defined on Circle.prototype. When you start programming with classes and objects in the real world, you'll undoubtedly notice that there's a certain amount of flexibility in the tools ActionScript provides for building class hierarchies and implementing inheritance. You'll likely need to adapt the approaches described in this chapter to suit the subtleties of your specific application.

12.5.4.2. Polymorphism

Inheritance makes another key OOP concept, polymorphism, possible. Polymorphism is a fancy word meaning "many forms." It simply means that you tell an object what to do, but leave the details up to the object (a.k.a. "Different strokes for different folks"). It is best illustrated with an example. Suppose you are creating a cops and robbers game. There are multiple cops, robbers, and innocent bystanders displayed on the screen simultaneously, all moving independently according to different rules. The cops chase the robbers, the robbers run away from the cops, and the innocent bystanders move randomly, confused and frightened. In the code for this game, suppose we create an object class to represent each category of person:

function Cop( ) { ... }
function Robber( ) { ... }
function Bystander( ) { ... }

In addition, we create a superclass, Person, that classes Cop, Robber, and Bystander all inherit from:

function Person( ) { ... }
Cop.prototype       = new Person( );
Robber.prototype    = new Person( );
Bystander.prototype = new Person( );

On each frame of the Flash movie, every person on the screen should move according to the rules for their class. To make this happen, we define a method move( ) on every object (the move( ) method is customized for each class):

Person.prototype.move = function ( ) { ... default move behavior ... }
Cop.prototype.move = function ( ) { ... move to chase robber ... }
Robber.prototype.move = function ( ) { ... move to run away from cop ... }
Bystander.prototype.move = function ( ) { ... confused, move randomly ... }

On each frame of the Flash movie, we want every person on the screen to move. To manage all the people, we create a master array of Person objects. Here's an example of how the persons array might be populated:

// Create our cops
cop1 = new Cop( );
cop2 = new Cop( );

// Create our robbers
robber1 = new Robber( );
robber2 = new Robber( );
robber3 = new Robber( );

// Create our bystanders
bystander1 = new Bystander( );
bystander2 = new Bystander( );

// Create an array populated with cops, robbers, and bystanders
persons = [cop1, cop2, robber1, robber2, robber3, bystander1, bystander2];

In every frame of the Flash movie, we call the function moveAllPersons( ), which is defined as follows:

function moveAllPersons( ) {
  for (var i=0; i < persons.length; i++) {
    persons[i].move( );
  }
}

When moveAllPersons( ) is invoked, all of the cops, robbers, and bystanders will move according to the individual rules associated with their class as defined by its move( ) method. This is polymorphism in action -- objects with common characteristics can be organized together, but they retain their individual identities. The cops, robbers, and bystanders have much in common, embodied by the superclass Person. They have operations in common, like knowing how to move. However, they may implement the common operations differently and may support other class-specific operations and data. Polymorphism permits dissimilar objects to be treated uniformly. We use the move( ) function to cause all the people to move, even though the move( ) function for each class is unique.



Library Navigation Links

Copyright © 2002 O'Reilly & Associates. All rights reserved.







??????????????@Mail.ru