| |
|
||||
|
Stacking Up Classes with __proto__ You may have noticed in the prototypes section that we used triple pane glass as a model, and yet we only really seemed to use the first two panes. As you have probably guessed, there are ways to 'add pane' to a simple two level object (ouch!). Before we get to that point, we must look deeper under the hood of an instance - right to the tailpipe in fact (ed. thanks for dropping the dentist metaphor here...). When an instance is asked for a property that it doesn't have, it automatically checks its constructor's prototype (the prototype of the class that created it). If the constructor's prototype does not contain the property, the story does not end. The constructor's prototype, which is also an object, will check the prototype of the class that created it - Object. So the prototype will check Object.prototype. That is a big bite of information, let's chew it a bit... Here is another 'stretched' metaphor - imagine your landlord asks you for the rent money. You don't have it (maybe not so hard to imagine), what do you do? Well, if you are like most people, you ask the object(s) that created you - your parents. Remember, you are wanting to give the landlord property you get from your parents (most landlords won't take your parents in lieu of rent) , so you are asking your parents for something in their prototype. What do your parents do if they don't have it either? After the 'little talk', they will tell you to go ask the object(s) that created them, your grandparents. Ignoring human mortality here for a second (not to mention inflation), your grandparents would tell you to ask their parents, who would tell you to ask their parents... You would keep being passed up the chain of your ancestors until you would either get the rent, or get to the giver of life, the creator of all things. Depending on just who 'the banker of last resort' is in your beleif system, that would either be the good lord, the government, or the Object.prototype. What if you ask everyone available, pray all night, and you still do not have the rent? Well, that's it - you do not have the rent, you have no choice but to pretend you are not home when the landlord visits. If any of these people had given you the rent, you would have happily taken it, and not have needed to ask anyone higher up the chain. Actionscript is the same, once it gets the rent property, it just gives it straight to the landlord and it's done (though it too 'feels' like celebrating with a couple two-fours and a trip to the casino). It may be useful to think of the prototype chain in this way until you are used to it - as a who-do-you-turn-to-when-you-don't-have-the-rent chain. Barring that, this may at least convince you to be a little more methodical come the end of the month. So how do objects 'know' who they are supposed to turn to when they don't have the rent? They are told when they are created. Who tells them this? The thing that creates them tells them, their parent. And what does their parent say? It says, "Listen closely my child, should you ever find yourself alone in this cold and barren world, in need of a property that you do not posess, come to me first and know you are welcome to use anything in my possesion". Before we get all misty, let's remember that classes store their properties in their prototype, so what this really means is that objects check the prototype of the class they were created with for properties they do not have. Sniff. Here is the rub: the prototype is an object (that is why you sometimes hear it refered to as the 'prototype object'). So what does this imply? It implies that if the prototype is asked for a property it doesn't have, it will check the prototype of the class that created it. Last question, what class creates prototypes? Maybe we know this already - remember prototypes are created automatically when functions are defined, and they are type 'object'. OK, maybe we didn't know that, but indeed prototypes are created by the class 'Object'. Like all other instances (all objects are instances, remember?) created with classes, they are told by their parent to "look in my prototype if you are ever short on the rent". So all prototypes, and thus all instances you create using your own classes, will eventually check Object.prototype for a property if they do not find it sooner. This is the third sheet of glass - Object.prototype. A major point of confusion: object and Object. All instances are objects, prototypes are objects, classes define objects... most things are objects of some type, created from a class of some sort. All these classes (and the objects they create) fit into a nice neat hierarchy. The top of this hierarchy then is obviously quite important, and needs a name. "Well Bob, I don't think it would confuse the shit out of people if we call that class 'Object', do you?". "Golly, clear as water to me Pete". And so it came to be called Object, with a capital 'O' (classes always start with a capital letter). We now have many types of objects, some of which are direct instances of Object, and some that are different types of objects but still inherit from Object. Though Bob and Pete have long since moved on to stellar careers as tax form designers, their legacy lives on. If you like, you can always substitute the word Object (the top level class) with the word 'TopDog' - try it on the above sentence and you will see how much more sense it makes. Where was I when they needed me...
To understand why all things inherit from the Object.prototype, it is important to really confirm to yourself that Object is itself just a class. What makes Object special is that it is the top level Class, much like 'living things' would be the top level class of a biological classification system. When you create an object by saying: x = new Object();
you are just making a call to this top level class, and creating the most generic type of object available in Actionscript (see side note). The Object class has two built in methods in its prototype, toString and valueOf. It also has one built in property that we will cover shortly. All objects will have these three properties. If they are created using the Object class, they will automatically check Object.prototype for extra properties. Objects created using any other class also have access to these properties - the class that creates them will have a prototype (all classes do) which was created by Object (all prototypes are), so the prototype will check Object.prototype for properties it doesn't have. The instance checks the class prototype, the class prototype checks Object.prototype. All classes use this prototype chain, regardless of whether they are your own classes, or built in ones. An array you create is an instance of the Array class. The Array class has a prototype, its prototype looks to Object.prototype for properties it can't find. The same goes for Color, Date and any other built in function you use to create new objects with. Their extra methods and properties (in their prototype) are what make them unique, however these objects can still do everything top level objects can do. By creating an instance of the Color class, you get an instance that is linked to the Color class's prototype. Your instance will also be linked to Object.prototype, because that is what Color.prototype is linked to. The situation works exactly the same when you create an instance of your own class. The chain for the rover example we have been so fond of looks like:
Object.prototype has a little sign on its desk that says 'The Buck Stops Here!'. Ok, we get all that then. So just what is the mechanism used that lets an object know where to look in times of need? This linking is made possible by __proto__. It is the third property in the Object.prototype we alluded to earlier, and therefore it is found in all objects. Its job is simple, it points to the object that should be checked next, if the current object doesn't have a requested property. In other words it stores the name of "who-to-turn-to-when-lacking-the-rent". So instance.__proto__ points to Class.prototype. The Class.prototype is also an object, so it also has a __proto__ property. Class.prototype.__proto__ points to Obejct.prototype. No one seems to remember if it was Bob or Pete that came up with the word __proto__, though Bob was sniffing a lot more glue at the time... You can easily remember it by the four underscores - think the four horsemen asking their parents for a couple hundred bucks to tide them over. It probably helps if you sniff glue. The dirty little secret of __proto__ is that it is NOT read-only. You can set it to point to anywhere. You can set rover's __proto__ to point to the Cat.prototype rather than Dog.prototype, and he will happily check there for the properties and methods that he doesn't have himself (OK, happily until he catches on). It's not just instances that can be manipulated like this either. You can tell a class's prototype to look for properties in places other than the Object.prototype. So the trick is to create two classes, and change the __proto__ property of the first one's prototype to point to the second one's prototype (instead of the Object.prototype). This way you will get a 'lower' class (child class, SubClass), and a 'higher' class (parent class, SuperClass). Think about it, one points to the other, the other (still) points to the top, so they are kind of stacked. How would you do this? Simply (conceptually if not syntactically) set the prototype of one, to point to (next-place-to-look-for-rent) the prototype of the other by changing its __proto__ property. SubClass.prototype.__proto__ = SuperClass.prototype;
Remember, we are not linking the classes themselves, we are linking their prototypes. Classes define instances, prototypes hold properties that instances can access automatically. One more quick thing to know about __proto__, is that it can be used multiple times to climb up the prototype chain. So instance.__proto__.__proto__ points to the SuperClass.prototype. This again is pretty intimidating syntax, but just count the red arrows - each one represents the four horsemen asking yet another ancestor for rent. Parent, Grandparent, Great-Grandparent, Great-Great-Grandparent... Whew, time for an example. We'll start with a few classes that aren't linked, but need to be. Hopefully this will shed a bit of light on why you would want to do this before we tackle how it's done. Here are a few animals:
// Dog Class Dog = function( name ) // Cat Class Cat = function( name ) { this.name = name; } Cat.prototype.legs = 4; Cat.prototype.price = 5; Cat.prototype.pet = true; // Hamster Class Hamster = function( name ) { this.name = name; } Hamster.prototype.legs = 4; Hamster.prototype.price = 15; Hamster.prototype.pet = true; You can see there is a lot of repetition happening here. Repetition generally makes a program less flexible and harder to maintain. What would happen if you wanted to add a new age category? You would have to add it to four places. It gets worse if you have to add a method, not only is there a lot of repeated code for each method, but if you find a bug in it, you now have to modify the code in each copy of it. If you miss even one letter in this process, you now have a new bug. This new bug seems like something else, so you change that in all the methods, and now you have a big mess. So what is to be done? Take a look at the above code and see if you can rearrange it before continuing. The solution is to make a parent category (superClass) called something like 'Pet'. All of these animals (and any others you may add) will inherit from Pet, so we can move general 'pet-wide' properties there. Lets go through the four properties one by one - name, legs, price, and pet - and decide where they belong.
Last thing to figure out, how is this done? Well, the inheritance from the Pet.prototype is quite simple. All that need to be done is to tell the Dog, Cat, and Hamster prototypes to look in Pet.prototype for properties they can not find (instead of the Object.prototype). This can be done by saying: Dog.prototype.__proto__ = Pet.prototype; Cat.prototype.__proto__ = Pet.prototype; Hamster.prototype.__proto__ = Pet.prototype; Armed with this knowledge, and knowing how we want to organize our properties, we can now write a fully functioning Pet class with multiple levels of inheritance - or so we hope anyway...
// Pet class Pet = function( name ) { this.name = name; } Pet.prototype.legs = 4; Pet.prototype.pet = true; // Dog class Dog = function( name ){ } Dog.prototype.__proto__ = Pet.prototype; Dog.prototype.price = 10; // Cat class Cat = function( name ){ } Cat.prototype.__proto__ = Pet.prototype; Cat.prototype.price = 5; // Hamster class Hamster = function( name ){ } Hamster.prototype.__proto__ = Pet.prototype; Hamster.prototype.price = 15; Lets try this out and make sure it works (in fact it doesn't fully work, can you see where the problem is?). rover = new Dog( "Rover" ); fluffy = new Cat( "Fluffy" ); ratboy = new Hamster( "Rat-Boy" ); for(var i in rover){ trace( i + ":\t" + rover[i] ) } /* output pet: true legs: 4 price: 10 */ for(var i in fluffy){ trace( i + ":\t" + fluffy[i] ) } /* output pet: true legs: 4 price: 5 */ for(var i in ratboy){ trace( i + ":\t" + ratboy[i] ) } /* output pet: true legs: 4 price: 15 */ The __proto__ property has done its job. The properties in the Pet.prototype (legs and pet) are available to each of the instances. The instances also pick up the properties from their constructor's prototype (price). The problem with the above code is that the name property seems to have dropped of the face of the earth. Poking around further, we can see that it is nowhere to be found. What went wrong? When we created a new instance, it ran the constructor block of code for that class. However, we moved the name property assignment into the Pet class, to save us from having to rewrite code for each type of Pet (all Pets have names). Yet this constructor never runs. You can use inheritance just like this, and keep all but the lowest level constructors empty. The technical term for this is 'three-legged-dog' inheritance (Ed. Is that true? I can't find any other book references to it.) Although it is possible to build useful programs in this way, we are very close to getting the full deal. Can you see how? We basically must get higher constructors to run before lower ones... When you have a program that isn't behaving the way you would like it to, the best thing to do try to isolate the problem. This almost always can come down to a single sentence and a few lines of code (if not you may have more than one problem!). So, to state our problem: Though our instances are inheriting properties from multiple levels via the prototype chain, only the first (lowest) level's constructor is being run. ...and to isolate it in a few lines of code: Pet = function( name ) { this.name = name; } Dog = function( name ){ } Dog.prototype.__proto__ = Pet.prototype; rover = new Dog( "Rover" ); // test trace(rover.name); // undefined At first one would be tempted to just run the Pet constructor every time a new instance of Dog is created, simply call it from the Dog constructor, right? Before the applause gets too deafening, it must be said, that this does not work. To understand why, we have to follow the scope very carefully, perhaps it helps to fall back on the 'objects are boxes' metaphor. Here's how it might look: Dog = function( name ) { Pet( name ); } Dog.prototype.__proto__ = Pet.prototype; rover = new Dog( "Rover" ); When Pet( ) is called here, it is called from inside the Dog class' activation object (which is the code block under the class). When Pet( ) is run as a method (not a class - it isn't called using the 'new' operator), the value of 'this' is set to whatever called it, so in this case it's set to the activation object. From what we know about activation objects, they are discarded after they are run. This obviously does not bode well for the name property that is attached to it, and indeed, it ends up in that great trash heap of discarded objects. What we have to do is run the Pet constructor, but from within the scope of the instance. There are two solutions to this problem, an easy awful one, and a more complex less awful one. Sadly there is no magic 'super' powered keyword to come to our rescue here. Well, easy one first... brace yourself, this gets ugly.
We know that 'this' in the Dog constructor represents the instance 'rover' being created (er, see side note). We know that when an object calls a method, the method's 'this' keyword will point to the calling object (this is what happened when we called Pet from the Dog's constructor above). So what we can do is make Pet a method of the instance 'rover', and then invoke it from the instance. Rephrased, 'rover' will have the code that is the Pet constructor as one of its properties, and we just get 'rover' to call it (as a method though, not as a constructor - no new operator). Let's say it a third time, only louder so we can understand - rover ate Pet and now it's inside him, so we run it from there. OK, so now rover has the method Pet right in him, and it looks comfortingly similar to the class called Pet. Now when Pet (the method) runs, 'this' will refer to 'rover'. All properties that are added to this end up in rover. Of course when this is all done, Rover will have an extra property that he doesn't need - the one that holds the Pet constructor reference - so you can just get rid of it (but don't step in it!). This property does not need to be named Pet of course, its value has to be equal to the Pet constructor, but its name can be anything. Well, almost anything. Anything that you are absolutely sure you will never use for a property name in an instance,otherwise your instance property will get deleted. '$_base' is probably safe for example, but just 'base' would not be (all your base will belong to trash heap). Here's how the code will look:
Yaay, it works! There are a few things to know about this though. First and foremost, this whole new $_base block of code MUST be the first thing in every constructor. This is true for all forms of inheritance, even in other languages like Java. This is VERY IMPORTANT (ed. looks much better without the 'F' word in there). The reason this is so important is that you have to run the constructors from top down, not bottom up. If you wish to override the name property in the dog constructor, you would put <this.name = 'Dawg'> in the Dog constructor. If you put it before the $_base block, it will set inst.name to 'Dawg', then it will run the $_base block, which will set the inst.name to 'rover'. So the higher class's constructor will overwrite the lower class's constructor. 'Death from above', as they say in the Girl Scouts. The second thing to know, and a bigger problem, is you now have two hard coded references to the parent class. Being that in OO development you are frequently shuffling classes around trying to find the best design, it opens up the very real possibility that you will forget to change one (it also is a extra code hunting time even if you don't forget). If you have the prototype chain lead to one class, and the constructor chain to another, all hell shall be cast upon thee. Very weird hard to track bugs. Anyway, the whole point of OO is trying to organize your program into units, and have the fewest possible points of failure on these units. So if a class is a logical unit, it should have a single link for inheritance (like all other OO languages). This may not sound like a big deal, but as a program scales, the above starts to become a burden. The biggest problem is that you end up not moving classes around when you should because it's too much of a hassle. That is lame, but it's true too. Probably something to do with the limitations of the human spirit. Last problem is more cosmetic, it makes your code harder to read. It is a pain to type, though you should really never make code decisions based on this alone. Typing it every time seems wrong though. Indeed, most people eventually create there own custom inheritance routines that automate this process, and we will too. These routines all have something like the above at the heart of them, so it is worth taking the time to be sure you understand it.. Before we create our own custom inheritance routines though, we must know what all we need them to do. So first a few more details, and a few more tools... Methods < < Home > > The Arguments Array
|