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."
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.
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.
A constructor function both defines our class and is used to instantiate new instances of the class. As we learned earlier, when invoked in combination with the new operator, a constructor function creates and then returns an object instance. Recall the general syntax for creating a new object based on a constructor function:
new Constructor( ); // Returns an instance of the Constructor class
So, to create a ball object (an instance) using our Ball class, we write:
myBall = new Ball( ); // Stores an instance of the Ball class in myBall
Our Ball class still doesn't add properties or methods to the objects it creates. Let's take care of that next.
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.
// 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.
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.
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.
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.
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."
To customize an inherited property for a single object, we can set a property on that object using the same name as the inherited property. For example, we might want to give one ball lower gravity than all other balls:
// Create our Ball class constructor function Ball ( radius, color, xPosition, yPosition ) { ... } // Not shown // Assign an inherited gravity property Ball.prototype.gravity = 9.8 // Create a Ball object lowGravBall = new Ball ( 200, 0x22DD99, 35, 100 ); // Override the inherited gravity property lowGravBall.gravity = 4.5;
A property set on an object always overrides any inherited property by the same name. This is simply a result of the way the inheritance chain works. Likewise, method definitions local to an object will override those in its class. If we execute ball1.area( ) and ball1 defines an area( ) method, that method is used; the interpreter never bothers looking at the Ball class's prototype to see if it also defines an area( ) method.
When a constructor's prototype object is created, the interpreter automatically assigns it a special property called constructor. The constructor property is a reference to the prototype's class constructor function. For example, the following expressions both yield a reference to the Ball constructor function:
trace(Ball); // Displays: [Function] trace(Ball.prototype.constructor); // Also displays: [Function] // Same reference as above
Note that the constructor property contains a reference to a constructor function, not a string representing the function's name.
When any object is created, the interpreter automatically assigns it a special property called _ _ proto_ _ (note the two underscores on either side of the name). The _ _ proto_ _ property of an object is a reference to the prototype property of that object's constructor function. For example, when we create an instance of Ball called myBall, myBall._ _ proto_ _ is set to Ball.prototype:
myBall = new Ball(6, 0x00FF00, 145, 200); trace(myBall._ _ proto_ _ == Ball.prototype); // Displays: true
The _ _ proto_ _ property is used primarily by the ActionScript interpreter to look up an object's inherited properties. For example, when we invoke the inherited method area( ) through myBall, as in myBall.area( ), ActionScript accesses the Ball.prototype.area method via myBall._ _ proto_ _.
We may also use _ _ proto_ _ directly to check whether an object belongs to a specified class, as shown in Example 12-6.
function MyClass (prop) { this.prop = prop; } myObj = new MyClass( ); if (myObj._ _ proto_ _ == MyClass.prototype) { trace("myObj is an instance of MyClass"); }
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.
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.
// 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.
// 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.
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.
Suppose we have a Shape class and a Rectangle class that has Shape as its superclass. To check whether an object is a descendant of Shape, we must refine the method in Example 12-6, to walk the chain of prototype objects (called the prototype chain). Example 12-9 shows the technique.
// This function checks if theObj is a descendant of theClass function objectInClass(theObj, theClass) { while (theObj.__proto_ _ != null) { if (theObj.__proto_ _ == theClass.prototype) { return true; } theObj = theObj.__proto_ _; } return false; } // Make a new instance of Rectangle myObj = new Rectangle( ); // Now check if myRect inherits from Shape trace (objectInClass(myRect, Shape)); // Displays: true
All objects descend from the top-level Object class. Hence, all objects inherit the properties defined in the Object constructor -- namely the toString( ) and valueOf( ) methods. We can, therefore, add new properties to every object in a movie by adding new properties to the Object class. Properties attached to Object.prototype will proliferate throughout the entire class hierarchy, including all internal objects and even movieclip objects! This approach can generate a type of truly global variable or method. In the following code, for example, we hardcode a stage width and height dimension into the Object class prototype. We can then access that information from any movie clip:
Object.prototype.mainstageWidth = 550; Object.prototype.mainstageHeight = 400; trace(anyClip.mainstageWidth); // Displays: 550
Object.prototype properties are inherited not only by movie clips but by every object, so even our own custom objects and instances of other built-in classes such as Date and Sound will inherit properties assigned to Object.prototype.
Reader Exercise: We can attach new properties and methods to any built-in class. Try adding the case-insensitive alphabetical sort function from Example 11-6 to the Array class as an inherited method.
In our survey of classes and class hierarchies, we've come across properties defined on objects, class constructors, and class prototypes. In Java and C++, there are specific names for the various types of class and object properties. For the benefit of Java programmers, Table 12-1 outlines the rough equivalencies between Java and ActionScript.
Classes and inheritance open up a world of potential for sharing common information among many objects. Object-oriented programming is the basis of all ActionScript. Whether or not you use every aspect of classes and objects in your scripts, understanding the general concepts involved is an essential part of understanding the Flash programming environment. As we'll see in the last section of this chapter, our confidence with object-oriented programming greatly influences our confidence with ActionScript in general.
For further advanced reading on object-oriented programming with ECMA-262-derived languages, see Netscape's documentation for JavaScript, Details of the Object Model:
http://developer.netscape.com/docs/manuals/js/core/jsguide/obj2.htm
David Flanagan's canonical text, JavaScript: The Definitive Guide (O'Reilly & Associates, Inc.) also provides valuable information on OOP in JavaScript. For a general introduction to OOP from a Java perspective, see Sun's Object Oriented Programming Concepts (from The Javatm Tutorial):
Copyright © 2002 O'Reilly & Associates. All rights reserved.