Chapter 13. Homemade Objects

CONTENTS
  •  Basic Objects
  •  A Practical Example of Homemade Objects
  •  Summary

Now that you've seen a variety of objects built-in to Flash, it's time to make your own objects. You've actually already had to create homemade objects when you made so-called "generic objects" for the purposes of storing multiple values in named properties (similar to arrays, except that each item is named). The homemade objects that you'll make in this chapter go beyond just storing data. They are almost identical to built-in objects such as the Movie Clip, Sound, or Date objects. The instances of homemade objects you create can have properties and methods.

In fact, homemade objects can be more sophisticated than those built-in to Flash because of a special feature called inheritance. After you design and build one object, it's possible to design new objects that share certain properties and methods of that object. For example, you could make a "Bank Account" object that includes a method to calculate interest. That same method can be inherited by another object (perhaps a "Certificate of Deposit" object). By using inheritance, you can modularize code to increase your productivity.

The process of creating objects is relatively straightforward and consistent with everything you've learned. Designing a good object, however, is where the challenge lies. The practical examples in this chapter should give you some ideas. Specifically, you will:

  • Create simple objects that maintain unique values for properties in each instance.

  • Create custom methods for object instances.

  • Assign prototype properties so that one object type can inherit properties and methods of another.

  • Change properties in "parent" objects so that the values of the same properties in their "children" will reflect the change.

  • Learn how to apply objects to practical Flash applications.

Basic Objects

When you see how easy it is to make an object, you'll probably be surprised. In fact, when you created associative arrays (or what I'd call "generic objects") in Chapter 11, "Arrays," and Chapter 12, "Objects," you were actually making objects. Individual objects are just a way to store multiple pieces of data in one variable. A quick way to both create and populate a generic object follows this form (which I call the "one-liner"):

myData={age:35,height:72,citizenship:"USA"};

From this point, you can access any item using the form:

myData["age"]=36;

The preceding form tends to look similar to arrays, but it's really easier to consider it a generic object. Let's do the whole thing over, but use a more formal "object way," as follows:

myData=new Object();  myData.age=35;  myData.height=72;  myData.citizenship="USA";

You can still access individual items (which are probably best called properties) the same way as shown earlier (myData["age"]) or by using the preferred dot-syntax with which we are so familiar (myData.age). This just proves that generic objects are associative arrays (and vice versa).

Although I tend to create, populate, and access my generic objects using the "object way" shown above (the second way), it's nice to know both ways. Of particular value is the way you can access the value of a named property when you might not know the exact property name. For example, if all you can do is build a dynamic string (say, "property_"+n, where n's value is 1 and you want the value for property_1), the only way to access that property is to use the form myObject[string] or myObject["property_"+n]. This technique should look familiar because it's the same as accessing a clip instance name when you only have a string version of the name, as in _root["clip_"+n]. Finally, the form of both creating and populating a generic object in one line of code is nice because it's so quick and easy. Some people call these short objects because they don't require the extra step of first declaring that you want to create an object. I still opt for the formal way of declaring the object and then populating it using dot-syntax, but it's good to recognize the options.

Using a Constructor Function

Imagine that you want several variables to contain objects, each of which contains properties for age, height, and citizenship. It would get rather involved to define all three properties every time you wanted to assign this object to a variable. Instead, you can create a function that serves as a constructor function (to instantiate new instances of the object):

function MakePerson(age,height,citizenship){   this.age=age;    this.height=height;    this.citizenship=citizenship;  }

Note

You may notice that my constructor function (MakePerson) appears with the first letter capitalized. This is really just a matter of convention. Programmers capitalize the first letter of a function that will be used as a constructor. Regular functions don't begin with capital letters.

This function accepts three parameters and uses their values to set the values for three (identically named) properties of "this." No rule says that parameter names must match the property names you're setting. (As you know, parameters are just temporary variables that represent values received.) Even though the MakePerson function accepts three parameters like a normal function, this function won't make a whole lot of sense if we call it as we would a normal function (MakePerson(22,68,"Canada"), for example), because it's only doing stuff to "this." Notice that the three lines inside set three properties of this. In Flash, you can use the this keyword in one of two ways: either to refer to the current clip (that you're "in"), or (as previously shown) inside a function to refer to the object being created by that function. You're about to see how you create objects (or spawn them) with a function, but not by simply calling the MakePerson() function that won't really do anything. Instead, you're going to use MakePerson() as a constructor function (to "construct" an instance of an object). It will be called as follows:

canadian=new MakePerson(22,68,"Canada");

Basically, we're saying the variable canadian will contain a new instance of the object created in the MakePerson constructor function, and then we send a few parameters to initialize property values. The reason for this in the constructor function is that we don't just want age set to 22, but rather canadian's age to 22. The this holds the place for the particular instance being created (canadian in this example). We can keep calling the constructor function to make as many instances of the MakePerson object that we want. For example:

mexican=new MakePerson(33,70,"Mexico");

We've simply made a constructor function that serves to create multiple instances of an object. All we had to do was define the properties each instance would have automatically (age, height, and citizenship). In the end, you'll have two variables containing objects (canadian and mexican). Both have a set of three properties each. For instance, canadian.age is 22 and mexican.age is 33. Placing all this code in the first keyframe will produce a structure of data visible through the Debugger (see Figure 13.1). Sometimes this might be the only way to verify that your objects are working.

Figure 13.1. Often the only way to see whether your objects are working is to use the Debugger.

graphics/13fig01.gif

Making Methods

Now that you know how to create a constructor function, we can move on to creating methods. First, we'll make a method that's hard-wired for just one instance of our MakePerson object, and then we'll improve on it.

You can add properties to a single instance by using the following form:

instanceVariable.newProperty=newValue;

For example, canadian.favoriteBeer="Lager" will create a favoriteBeer property and set its value to "Lager". This isn't terribly exciting (or anything new, if you recall creating variables that acted like properties of clip instances). What's really wild is that by assigning a property's value to equal a function, you'll actually be making a method. For example, consider this code:

function incrementAge(){   this.age++;  }  canadian.haveBirthday=incrementAge;

Because incrementAge is really the name of a function, the instance canadian now has a method called haveBirthday(). Therefore, every time the script canadian.haveBirthday() is executed, the age property for the instance canadian is incremented. Notice a couple details above. The function incrementAge is referred to simply by its name, but not the form incrementAge() (as perhaps expected). That's because we don't want to actually call the function (like normal, with the parentheses), we just want to point to it. Also, it's possible to consolidate the two pieces above into one (arguably more confusing) line of code. That is, the following code achieves the same result with one slight advantage:

canadian.haveBirthday=function(){this.age++;}

The advantage of this consolidated form is that the incrementAge isn't sitting around taking up space (in memory). I think the first way is easier, and that's really the only reason I did it that way. Just like variables, if you create a function that only gets used once, you didn't really need it in the first place. It doesn't hurt much, so I'll tend to do it this way for readability. Finally, notice that I never give the function a name in my one-liner above. It actually appears to break the form for functions. To say, "Have birthday equal this function," kind of makes sense, however.

Although making a method for an individual instance is pretty cool, it has the definite drawback of being hard-wired just to the canadian instance. Most likely, when you spend the time to develop a method, you'll want to be able to apply it to every instance of a particular type of object. After all, you're allowed to apply the nextFrame() method to any instance of a movie clip so why not be able to apply the haveBirthday() method to every instance of the MakePerson object? The way we just did it will not allow you to use mexican.haveBirthday(). We only specified this method for the one instance (canadian).

To create a method that will apply to every instance of a particular object, you use a special property, called prototype, that's built-in to every object. The prototype property is an object, so it has several properties of its own. You can individually specify as many properties of the prototype object (so as to create methods) by specifying a function in each one. The methods in the prototype object will apply to every instance spawned from the original constructor function. So, in the preceding script, rather than the line:

canadian.haveBirthday=incrementAge

use the following:

MakePerson.prototype.haveBirthday=incrementAge;

Translated, this says that the MakePerson object's prototype now includes a property called haveBirthday, which is assigned the value incrementAge. (Because incrementAge is really a function, this means we've made a method called haveBirthday() a method.) And because it's the special prototype property that we just added a property to, all instances created from MakePerson will now have access to this method. Therefore, you can do both canadian.haveBirthday() and mexican.haveBirthday(). The result is that we've made a method for our MakePerson object!

Remember that because the prototype property's data type is an object, it can contain multiple properties of its own. We've added only one property (really a method because it's a function) to the MakePerson's prototype property: haveBirthday. We can add more properties or methods:

function growAnInch(){   this.height+=1;  }  MakePerson.prototype.grow=growAnInch;

Notice that this last line doesn't wipe away the haveBirthday method already contained in MakePerson's prototype; we just added a new method (called grow()).

So far, we've talked only about adding methods to your objects. However, if you want to add a property (not a method), you first have to decide whether this new property's value should be the same for each instance or whether it should be maintained individually for each instance. For example, if you decide that your MakePerson object should include an additional property called weight, obviously each instance should have its own value for weight. (That is, the mexican can weigh a different amount than the canadian.) However, if you want to add a property such as species, it makes sense that this has the same value for each instance. A property that's the same for each instance is more accurately called a constant (and as such could be written in all capital letters as a matter of convention, such as SPECIES). Let me explain three different ways that you can add properties.

To create a new property for just one instance of an object, use the familiar form instance.newProp=value. If you want to create a new property that will be maintained individually for every instance created, you must go back to the original constructor function and add a line such as this.weight=weightParam. Finally, if you want to add a property that serves as a constant because it is the same for every instance, you need to add it to the prototype property, as follows:

MakePerson.prototype.SPECIES="homo sapiens";

You might think that you could just hard-wire this SPECIES property inside the constructor function (that is, not set this.SPECIES to a value passed as a parameter, but just hard-wire "homo sapiens"). You could. What's cool about using to the prototype property is that later, with one swoop, you could change the value of any property for all instances ever created (including those yet to be created). Just execute the following code:

MakePerson.prototype.SPECIES="alien";

The previous analogies included physical human characteristics to hopefully make some concepts about objects clearer. The problem, however, is that it's nearly impossible to quickly extrapolate this "MakePerson" theme into a practical Flash application. Any time you work with objects (unless they are Movie Clip instances), it can be difficult because while you work, it's still kind of ephemeral. Ultimately, after you build an object, you can use it in conjunction with visual elements that appear onscreen. But the work involved creating clips and graphics can be pretty consuming. My suggestion is that you get very familiar with the Debugger (so that you can watch your variables as they change) and use trace() to test what you build every step along the way. For example, I typed the following block of code into the first frame of my movie, and then I opened the Debugger to watch my variables. Finally, I created some buttons so that the values of various expressions would appear in the Output window (see Figure 13.2). This way, I could be sure that all the code I produced was working:

 1 function MakePerson (age, height, citizenship) {  2   this.age = age;   3   this.height = height;   4   this.citizenship = citizenship;   5 }   6 canadian=new MakePerson(22,68,"Canada");   7 mexican=new MakePerson(33,70,"Mexico");   8 MakePerson.prototype.birthday=incrementAge;   9 function incrementAge(){ 10   this.age+=1;  11 }  12 MakePerson.prototype.SPECIES="homo sapiens";
Figure 13.2. Combining the Debugger and the trace() function along with rough buttons enables you to see how your objects are working.

graphics/13fig02.gif

As a review up to this point, the first five lines are the constructor function that we learned to create earlier in this chapter. Lines 2 4 set properties that can be unique to each instance in the form this.property=value (where value can be passed as a parameter). Actually, the contents of the function itself aren't really important; it's just a regular function. The function becomes a constructor function only when we invoke it by using the new command. We create an instance by saying myInstance=new functionName(), as in lines 6 and 7. Finally, we tapped into the object's prototype property. As an object itself, the prototype property enables us to assign values to as many named sub-properties as we want. To create a method, we simply assign one of the prototype's properties the value of a function, as in line 8. Any other data type, as in line 12, will act like a global property or constant (the value of which will be the same for every instance). Figure 13.3 shows the form for the various maneuvers we've looked at:

  • Writing a constructor function.

  • Writing a function that will become a method.

  • Creating instances of objects by invoking a constructor function.

  • Creating a method by creating a property of the prototype property.

  • Making a global property (one that is part of every object) instantiated with the constructor function.

Figure 13.3 The syntax for various object-related maneuvers.
constructor function:  function obj(param){   this.prop=param;  }  function that will become a method:  function doIt(){ }  creating instances (by invoking "new" constructor):  inst1=new obj("x");          //inst1.prop == "x"  inst2=new obj("y");          //inst2.prop == "y"  making a method  (by putting a function inside a  property of the object's prototype property)  obj.prototype.myMethod=doIt;  //prototype.myMethod == a function  making a global property  (by putting any data type--except function--inside a  property of the object's prototype property)  obj.prototype.CONST="value";  //prototype.CONST == a string

Inheritance

The prototype property can do more than just maintain methods and constants. You're about to see how you can write a generic method for one object, and then recycle that same method in another object. This is similar to the way you can write one generic function that you call from several places in your Movie Clip. It's different, however, because you actually make one object inherit all the properties and methods of another. Consider how a child can inherit a base of attributes from his parent, but then goes on to develop his own. Similarly, we can write a generic object (with a set of methods) that allows other objects to inherit (and thus recycle) the entire set of methods.

In the example that follows, first you'll create a "Bank Account" object that maintains a balance and interest rate for each instance. You'll write a method that compounds the interest on the current balance. Then, you'll make another object: a "Certificate of Deposit" (or "CD") object. Rather than writing a unique method to compound interest on the CD object, you can just use the same method from the Bank Account object. The Bank Account object is the generic template. The CD object inherits all the attributes (that is, the properties and methods) of the Bank Account object (see Figure 13.4).

Figure 13.4. The CD object will have all the same properties and methods of the Bank Account object, plus a few of its own.

graphics/13fig04.gif

In this way, the CD object is everything that the Bank Account object is and more. Your head might spin if you consider that you can have a deep hierarchy of objects that inherit attributes of other objects, which, in turn, have inherited attributes from other objects. Although explaining how to do it is easy, applying this knowledge is another matter entirely.

The way you build an object that inherits the methods of a parent object is actually quite simple. Consider the following simple bank account constructor function (BankAct) that you might put in the first keyframe of a movie:

function BankAct(startingBalance,interestRate){   this.balance=startingBalance;    this.rate=interestRate;  }

You probably won't want to start cranking out instances of the BankAct object until you've worked out the methods and inheritance that follow. When you do, however, you'll make an instance (that you store in a variable called primarySavings, for example), by using:

primarySavings=new BankAct(5000,.04);

To create the compound() method for all instances of the BankAct object type, use the following two statements:

function multiplyAndAdd(){   this.balance+=(this.balance*this.rate);  }  BankAct.prototype.compound=multiplyAndAdd;

In this case, there's no compelling reason to use a different name for the method and function; multiplyAndAdd could just as well be called compound. In the following section, "A Practical Example of Homemade Objects," I'll show a reason why you might like to keep these names separate. With this method built, you can increase the balance in any instance of the BankAct object with a simple call, as follows:

primarySavings.compound();

This script will cause the object stored in primarySavings to increase its balance based on interest rate. You can type trace(primarySavings.balance) before and after the preceding line to verify that the compound() method is working.

Now that we have this somewhat simple compound method, we might want to use it again for other types of objects. It only makes sense to use the compound method on other object types that maintain a balance and interest rate. Let's make a new object (a "CD" object) that will have all the same properties and methods of our BankAct object, but that will also have its own set of unique properties and methods. All "certificates of deposit" are "bank accounts," but not all "bank accounts" are necessarily "certificates of deposit." So, here's the constructor function for a CD object:

1 function CD(startingBalance,interestRate,lengthOfTermInDays){ 2   this.balance=startingBalance;  3   this.rate=interestRate;  4   this.term=lengthOfTermInDays;  5   var now=new Date();  6   this.renewalDate=new Date();  7   this.renewalDate.setDate(now.getDate()+this.term);  8   this.longDate=this.renewalDate.toString();  9 }

In addition to initializing the properties balance, rate, and term (based on the parameters received), this function performs some Date object operations to set two other properties (renewalDate and longDate). Line 5 assigns a local variable (now) to the current time, line 6 initializes renewalDate as a date object type (that is, its data type is the date object), and line 7 resets renewalDate by way of the setDate() method (pushing it out term days from now). Finally, last line creates a string version of the renewalDate property really just for readability, because we'll never use this property within a calculation, such as in the following method:

function extendDate(){   this.renewalDate.setDate(this.renewalDate.getDate()+this.term);    this.longDate=this.renewalDate.toString();  }  CD.prototype.renew=extendDate;

Notice that we don't ever use longDate; we just reassign it after adding term to the current renewalDate. The last line (outside the function) is the way that we make extendDate() become a method (called renew()) of the CD object.

So far, we've built two objects (BankAct and CD) and a method for each. At this point, we could duplicate the code for BankAct's compound() method and make an identical method for CD; however, I hate to repeat code I don't have to. We can specify that the CD object should inherit all the properties and methods built-in to the BankAct object. Notice that both objects have properties for balance and rate. When you inherit, all properties and methods are inherited. The fact that both CD and BankAct objects have some of the same property names doesn't necessarily matter. However, the instant CD inherits everything from BankAct it actually overwrites any properties or methods it already has. The point is, the sequence of your code does matter. For example, if you store an instance of the CD object in a variable and then say, "The CD object will inherit everything from the BankAct object," the variable already created will be unaffected. (Compare this to how changing the default behavior of a symbol in the Library from Button to Movie Clip will have no affect on instances already on the Stage.) However, if you create a CD object after the "inherit now" statement, it will be a child of the BankAct object. Unfortunately, saying "inherit now" (the way I'm going to show first) causes all previously defined methods to be wiped away. I'll show some examples later, but first consider the simplicity of the following code, which causes the CD object to inherit everything from the BankAct object:

CD.prototype=new BankAct();

That's it. Translated, this simply says the CD object is a subset of the BankAct object and will inherit all its properties and methods. As you'll recall, defining a method for the CD object added to its prototype (as in CD.prototype.methodName=aFunction). The inherit code above is different in two ways. First, it sets the whole prototype equal to something not just by adding a property, but by replacing prototype entirely. Second, prototype is being assigned to a new instance of the BankAct object (notice the word new). You could read the preceding code as "CD's prototype the whole thing is now equal to the BankAct constructor and all its properties and methods."

From this point forward, you can create as many CD objects as you want. And if you ever need to compound() the balance, you can do so, as follows:

rainyDay=new CD(2000,.08,180);  rainyDay.compound();

The first line creates an instance of the CD object and places it in the rainyDay variable. The second line invokes the compound method that was naturally inherited from the BankAct object (with the line CD.prototype=new BankAct();). The only little problem is that when CD inherited everything from BankAct, it wiped away all methods including the CD object's renew() method. We can solve this problem a couple of ways. Realize the fact that assigning the child object's entire prototype to another object will wipe away methods defined earlier. One easy fix is to simply define any methods for the CD object after the "inherit now" statement. Here's the entire code, with some comments to clear things up:

//constructor for BankAct object  function BankAct(startingBalance,interestRate){   this.balance=startingBalance;    this.rate=interestRate;  }  //define and create compound() method  function multiplyAndAdd(){   this.balance+=(this.balance*this.rate);  }  BankAct.prototype.compound=multiplyAndAdd;  //constructor for CD object  function CD(startingBalance,interestRate,lengthOfTermInDays){   this.balance=startingBalance;    this.rate=interestRate;    this.term=lengthOfTermInDays;    var now=new Date();    this.renewalDate=new Date();    this.renewalDate.setDate(now.getDate()+this.term);    this.longDate=this.renewalDate.toString();  }  //make CDs inherit everything from BankAct  CD.prototype=new BankAct();  //define and create the renew() method (now that it's safe to add to CDs)  function extendDate(){   this.renewalDate.setDate(this.renewalDate.getDate()+this.term);    this.longDate=this.renewalDate.toString();  }  CD.prototype.renew=extendDate;  test=new CD(100,.1,30);  trace ("init: " + test.balance + " "+ test.longDate);  test.compound();  trace ("after compound: " + test.balance + " "+ test.longDate);  test.renew();  trace ("after renew: " + test.balance + " "+ test.longDate);

As previously mentioned, the order matters. Notice that I didn't start adding to the prototype property of the CD object (to create the renew() method) until after I said, "Inherit everything from the BankAct object." Finally, I threw a few test lines at the end to verify that both the inherited compound() method and the renew() method were indeed working.

Although my careful attention to the order made the previous example work out, there's another way to inherit. Notice first, though, that the code child.prototype=new parent() inherits everything properties, methods, and constants. For example, even if the CD object didn't create the balance and rate properties; it would still have them because the BankAct object does. Basically, anything inside the parent object's constructor becomes part of the child object. The other way to specify inheritance differs because you just say, "Inherit just the methods of the parent object but not any properties in its constructor." The appearance of the code can be intimidating, but it's really not too bad. To understand, first ask yourself: Where are an object's various methods stored? They're stored as named properties inside the object's prototype property (for example, anObject.prototype.method=function). So, if we want the child object to acquire a parent's set of methods, we just need to point to that parent's prototype. We can't just say child.prototype=parent.prototype,however, or we'll be overwriting the child's entire prototype and, subsequently all its methods. We just want to add to the child's set of methods.

You also need to know about a special built-in property of the prototype property: __proto__ (notice the two underscores on each side). That is, every object automatically gets a prototype object into which you can name properties that hold methods. In addition, each prototype object has a built-in special property, called __proto__, which can optionally point to a parent object's prototype to acquire a set of inherited methods. The following code shows how to declare that you want a child object to inherit only the methods of a parent object:

child.prototype.__proto__=parent.prototype;

For our example, the code would be as follows:

CD.prototype.__proto__=BankAct.prototype;

That's equivalent to saying CD.prototype=new BankAct(). The difference is simply that any code from BankAct's constructor will not be included in all CD objects. In this example, that's not a big deal because the following two lines appear in both the CD and BankAct constructor:

this.balance=startingBalance;  this.rate=interestRate;

The way I separate the two ways of inheritance is that one simply extends an object (the __proto__ way) and one redefines and overwrites an object (the first way).

A Practical Example of Homemade Objects

For a practical example, let's consider a database full of products for sale through a music and video store. Figure 13.5 shows the start of a rough layout.

Figure 13.5. This rough draft of our next exercise will explore every aspect of homemade objects discussed in this chapter.

graphics/13fig05.gif

The basic features we're going to build include a way to first store the database of products in one big list and then to use the arrow keys to browse one item at a time. The selected product will have details such as its price and title provided in the text above. In addition, a button will enable a person to reduce the price by an amount indicated on a coupon. Finally, to demonstrate a constant (or "global property"), we'll be able to change the store name at any time. Obviously, this isn't the finished product, but it should offer a great way to see how to apply objects. By the way, almost all the scripts are placed in the first keyframe. In addition, there are just a few buttons (such as the "next" and "previous" buttons) that call various functions, such as move(). These are all shown within mouse events (such as on (release)).

First, we will design the objects. The most generic object will be called Product and will contain both a price and a sku (for "Stock Keeping Unit" a unique number for every stock item). In addition, we'll build a method that allows for a discount to be applied to the price. Here's the code to do what I've specified so far, which can be placed in the movie's first keyframe:

function Product(price,sku){   this.price=price;    this.sku=sku  }  function coupon(faceValue){   this.price-=faceValue;  }  Product.prototype.discount=coupon;

The first function is the constructor for the Product object; the second function (coupon) turns into a method (called discount()), which, in the last line, is placed in Product's prototype property. The Product object is the most basic form from which all other objects will inherit these properties and methods. (At this point, there's only the discount() method.)

The types of products this store currently sells are audio CDs and videos (in the VHS and DVD formats). Instead of locking those down (and preventing diversification in the future), we'll consider all these products of the type "media." To that end, first we'll design a Media object that inherits everything from the Product object. And then the various objects that follow (CD, DVD, VHS) will inherit everything from the Media object (and, therefore, from Products as well). By placing the following code below the preceding code, we can make the Media object act as a descendant of the Product object:

function Media(){ }  Media.prototype=new Product();

Notice that the Media constructor doesn't do much. You might expect it to at least set a property or two. In the future, I could make plain Media objects that maintain their own properties, but I have no plans for that now. I could also create a method for the Media object, and all descendants would inherit that as well. (I'm not going to for this example, however.) For now, I just want to ensure that all Media objects (and their descendants) inherit everything from the Product object. (By the way, if you don't want this extra level of hierarchy, you can change the code below that reads new Media() to read new Product(), and it will still work.)

Now we can make a separate object for audio CDs (CD) and one for videos (video) that both inherit everything from the Media object. Each object will be a subset of the Media object (and, therefore, of the Product object), so they'll have the discount() method. In addition, each object will have its own unique set of properties and methods. Let's just make the objects first, as follows:

function CD(title,lyrics,price,sku){   this.title=title;    this.lyrics=lyrics;    this.price=price;    this.sku=sku;  }  CD.prototype=new Media();

The last line of the preceding code establishes that CDs inherit everything from the Media object.

The following constructor for Video objects follows a similar form as that of the CD object constructor:

function Video(format,title,rating,price,sku){   this.format=format;    this.title=title;    this.rating=rating;    this.price=price;    this.sku=sku;  }  Video.prototype=new Media();

Now that we have the constructor functions for the Video and CD objects and have established that they inherit everything from Media objects, we can create as many CD or Video instances as we want. First, I would like to write a method that works with either videos or CDs to determine whether the content is approved for children. If a video's rating property is not "R" and not "X," the okayForKids method (that we're about to build) will return true. Similarly, if a CD object's lyrics property is not "EXPLICIT," the okayForKids method will return true. Initially, you might consider writing the okayForKids method for the Media object. This makes sense when you consider that you want to be able to use the same method with either object type. However, the decision whether it's "okay for kids" is based on a different property in each object. Instead, let's write two versions of the method. The following start has some problems, which we'll address shortly:

function okayForKids(){   if (this.lyrics ! = "EXPLICIT"){     return true;    }else{     return false;    }  }  CD.prototype.okayForKids=okayForKids;  function okayForKids(){   if (this.rating ! = "X" && this.rating<>"R"){     return true;    }else{     return false;    }  }  Video.prototype.okayForKids=okayForKids;

This solution won't work because both the function declarations have the same name. Although it's desirable that both the methods are given the same name (so that we can invoke them the same way someCD.okayForKids() or someVideo.okayForKids()) we can't have two constructor functions with the same name. Consider the following modified (and usable) solution:

function notExplicit(){   if (this.lyrics ! = "EXPLICIT"){     return true;    }else{     return false;    }  }  CD.prototype.okayForKids=notExplicit;  function notX_notR(){   if (this.rating != "X" && this.rating<>"R"){     return true;    }else{     return false;    }  }  Video.prototype.okayForKids= notX_notR;

Earlier in the chapter, I said the function that defines how a method will work can have the same name as the method, but it isn't always desirable. In this case, we made both of them different so that there would be no overlap. Notice, however, that Video objects have an okayForKids() method, and so do CD objects. It's cool that despite checking for slightly different things, they both have the same name. This is called polymorphism. Consider how many different professions could have a "get certified" method. It would be called "get certified" regardless of whether you were a building inspector or a lion tamer.

One tiny thing to add is a store name property that can be changed (for all product objects and their descendants) in one move. Although we already have a method for the Product object (discount()), we can add a constant (one that is not unique for every object instance) by using the following line:

Product.prototype.STORE="Phil's Media Shop";

This specifies a STORE property (and its value) for all Product objects (and all descendants).

Finally, we can start using this code to make object instances! From this point forward, we're simply going to use objects in an application that is, we're done defining the structure. The following discussion is regular "Flash stuff." First, we can make a few objects:

cd1=new CD("Music for kids","JUVINILE",12.95, 11222);  cd2=new CD("Hate Music",    "EXPLICIT",13.95, 22311);  cd3=new CD("Musicals",      "MUSICAL", 8.95,  42311);  dvd1=new Video("DVD","Explosion!","R",     19.95, 23122);  dvd2=new Video("DVD","Colors",    "PG-13", 19.95, 2233);  vhs1=new Video("VHS","Horses",     "G",    9.95, 2344);

Lastly, we want to store all our objects in an array so that we can step through them one at a time:

allProducts=[cd1,cd2,dvd1,vhs1,dvd2,cd3];

Now we can do a few exercises. Assume that we want to determine the price for the second product in allProducts:

allProducts[1].price;

Notice that we can grab the price property of an object by first referring to the array (full of objects).

If we want to discount the price of a product by 1.95, we can use

allProducts[1].discount(1.95);

The result would be that cd2 now costs 12.

You can see how easy it is to work with this storeful of objects. Let's build two arrow keys that step through each product and display data in a Dynamic Text field. Just make one button containing move(1) (inside a mouse event, of course) and one with move(-1). You should write the move() function and the updateDisplay() function in a keyframe of the main timeline, as follows:

function move (direction) {   curItem -= direction;    curItem=Math.min(allProducts.length-1, curItem);    curItem=Math.max(0, curItem);    updateDisplay();  }  function updateDisplay(){   descript_txt.text= allProducts[curItem].price;  }

Basically, the move() function increases or decreases the value for curItem in the second line. Also, by using Math.min(), curItem is ensured of staying below the highest index in allProducts; by using Math.max(), curItem is ensured of staying at 0 or higher. Then, the last line of the move() function calls the updateDisplay() function, which sets the text property of the field descript_txt to the price of curItem. I didn't just put the code from the updateDisplay() function inside the move() function because I'd like to be able to invoke updateDisplay() from elsewhere as well. Finally, we'll need a Dynamic Text field with the instance name descript_txt. (Be sure to make this field a tall box with multiline and word wrap selected (see Figure 13.6), because we will be adding more information later.)

Figure 13.6. Because the string we're going to display has a lot of information, make sure to specify multiline and word wrap to accommodate it.

graphics/13fig06.gif

Notice that the only data being displayed is the price. We'd like to display much more data, however (such as its title, its type of media, and whether it's suitable for children). Check out the replacement version of the updateDisplay() function:

function updateDisplay(){   var theObj= allProducts[curItem]    var str= theObj.store + " is proud to offer \r";    str=str + "A " + theObj.format;    str=str + " called " + theObj.title;    str=str + " for only " + theObj.price;    descript_txt.text=str;  }

We need to call this updateDisplay() function every time there's a change (that is, when we call the move() function). In addition, however, we should call this function right after we populate the array. The last line of the code in the first keyframe, after allProducts is initialized, should include a call to updateDisplay(). But it should work.

Now that we have built the structure (and it's sound), we can start adding sophisticated features with ease. For example, if the product is not "okay for kids," we can include an additional warning just by adding the following if statement to the end of the updateDisplay() function:

if (!theObj.okayForKids()){   descript_txt.text=descript_txt.text + "\r\r Sorry, customers over 15 only please."  }

Notice the exclamation point (known as a logical not), which changes the meaning of the condition to "if not okay for kids."

You can keep adding features to this with little effort. To change the store name, for example, just combine an Input Text field associated with a variable newStoreName with the following code in a button:

on (release) {   Product.prototype.STORE=newStoreName;    updateDisplay();  }

Because store is a global property, the preceding code will change the value of STORE for every Product object, including descendants.

You can do the same thing to let users reduce the price of an item. Just make an Input Text field with the variable couponValue, and then make a button with the following code:

on (release) {   allProducts[curItem].discount(couponValue);    updateDisplay();  }

Notice that both these buttons call the updateDisplay function.

Although this is pretty complete, I want to add one more variation for no other reason than to explore another application. Let's assume that our store wants to start selling food as well as media products. I'm going to make another object type equivalent to the hierarchy of the Media object and call it Food. Later, I could make sub-object types to Food (the way CDs and videos were types of media), but I'm not going to go that far. This is all I did:

function Food(name,price,sku){   this.name=name;    this.price=price;    this.sku=sku;  }  Food.prototype=new Product();

Then, in the place where I instantiated all my objects, I simply created one more: food1=new("Potato Chips",.55,12223). I also made sure to include it when I populated allProducts:

allProducts=[food1,cd1,cd2,dvd1,vhs1,dvd2,cd3];

Everything should still work, except that because the updateDisplay function includes the object's title and format, there's a problem in that Food instances have only name, price, and sku properties. We can modify the updateDisplay function to say (in pseudo-code), "If it's a media descendant, do what we were doing; otherwise, build a different string." There are a couple of ways to determine an object's parents. It can get pretty hairy because an individual CD is the child of the CD object, which is a child of the Media object, and so on. To verify that an object's __proto__ is equal to another object's prototype isn't enough. For our application, we'd have to check whether an object's __proto__'s __proto__ was equal to the media object's prototype. (To check whether an object was a descendent of the product object, we'd have to do something to verify that cd1.__proto__.__proto__.__proto__==Product.prototype was true.)

graphics/icon02.gif

No thank you! Luckily, Flash MX has added a handy operator, called instanceof. All we need to do is make an expression (such as oneObject instanceof otherObject), and it will evaluate to true or false, depending on whether oneObject is a descendant of otherObject. For our application, we just need to check whether the object in question is an instance of Media or Food. To apply this to the updateDisplay function, we just need to make the following adjustment:

function updateDisplay(){   var theObj= allProducts[curItem]    var str= theObj.store + " is proud to offer \r";    if (theObj instanceof Media){     str = str + "A " + theObj.format;      str = str + " called \"" + theObj.title;      str = str + "\" for only " + theObj.price;      if (!theObj.okayForKids()){       str = str + "\r\r Sorry, customers over 15 only please."      }    }    if (theObj instanceof Food){     str = str + "A delicious " + theObj.name;      str = str + " for only " + theObj.price;    }    descript_txt.text= str;  }

The coolest thing about objects is that after they're built, you can add layers of features and significantly change your program without having things fall apart. The hard work is in designing the objects so that they make sense for your application. After they're built, they're easy to modify.

Summary

The truth is that you can live your whole life without once creating an object. You can also create simply amazing Flash sites without them, too. It's just that they're so darn convenient as a way to handle complex data. Plus, you're about to see how to extend Flash's object model to override a built-in object's prototype property. Yes, even the Movie Clip has a prototype object full of methods. Not only can you make your own, but you can re-write Flash commands, such as gotoAndPlay()! The good news is that all the knowledge from this chapter will help you in Chapter 14, "Extending ActionScript." In the practical example we built, the code structure wouldn't need to be modified at all if you decided to add thousands of products. (You'd have to instantiate those thousands of items and populate the allProducts array, of course.) The visual representation of data stored in objects is a bit of work but then objects aren't supposed to create graphics for you. I see them as a ton of upfront work that pays back only when you can use them repeatedly.

As a quick review:

  • You learned how to make custom objects.

  • You learned how to create custom methods by using the prototype property (which is an object in itself because it has multiple properties).

  • Finally, you learned that all attributes are inherited when you associate an object type's prototype with another object's new constructor function.

CONTENTS


ActionScripting in MacromediaR FlashT MX
ActionScripting in MacromediaR FlashT MX
ISBN: N/A
EAN: N/A
Year: 2001
Pages: 41

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net