So I ran into a bit of a snag yesterday when I was working for the first time with the most excellent Greensock TweenLite ActionScript class. Naturally I panicked and blamed everything on the class. Fortunately the author of the class, Jack Doyle was listening in on his forums and responded to my surprise within minutes of my posting the query there. He assured me that his class was iron-tight and pointed me in a direction that led me to uncover the error of my ways.
How naive I was! Ah, the shame of it all.
So, to hopefully forestall any future embarassment on the part of you, my reader, who has miraculously stumbled upon this obscure piece of flotsam in the vast ocean that is the InterMegaWeb, let me expound upon the dangers of ActionScript’s pass-by-reference.
But first, let me set the stage by introducing TweenLite a little. Although the class has absolutely no bearing whatsoever on the problem I was experiencing, the way the class is designed kind of allows one to fall into the trap fairly easily, and makes a good example. And besides, it’s a very useful class, so if this is your first introduction to it, I will consider that I did you a service.
TweenLite serves to bolster ActionScript support for tweening things over time without using the Timeline or the Flash IDE. For those of us foolish enough to try to animate things using code, the built-in native Flash Tween class is woefully poor, performance-wise. There are tests to prove it (beware: when testing the native Flash Tween class, be sure to lower the values as it will crash your browser – that’s just how bad the Flash class is!). The TweenLite tween class is a joy to use.
Let’s say you want to tween a MovieClip named myClip across the stage from x = 100 to x = 500, and from y = 100 to y = 400, and you want it done over 5 seconds:
TweenLite.to(myClip, 5, {x:500, y:400});
The previous example assumes that myClip is already at x = 100 and y = 100. Over the next 5 seconds, it will move its way toward (500,400) incrementally each frame. You don’t have to do a damned thing but sit back and watch. Very sweet.
You can even add other, custom parameters to TweenLite to tweak its behaviour. Say you wanted a 1-second delay before the object actually started to move:
TweenLite.to(myClip, 5, {x:500, y:400, delay:1});
Simple. And then of course I had to get in there, try to make things even more user-friendly and that’s when stuff starting blowing up.
Blowing Up Soft References
Since I’m always thinking about making my code more readable and more maintanable by people other than myself, I like putting things like parameters that affect visual display and may need to be tweaked, up-front near the top of my code. Since TweenLite.to takes an Object (an anoymous one in the example above as identified by the { }), I figured – hey, let’s create an object to hold these parameters. I was tweening more than one property at a time so let’s take those properties, stuff them into the object and then retrieve them as needed. Let’s see how this looks:
private var tweenParameters:Object = {
x:500,
y:400,
alpha:.5
}
And then later on in the code I use this data to populate various calls to the TweenLite class to tween different MovieClips:
var paramsOne:Object = tweenParameters; paramsOne.delay = 1; TweenLite.to(myClip, 5, paramsOne); var paramsTwo:Object = tweenParameters; TweenLite.to(myClip, 5, paramsTwo);
Here, what’s supposed to be happening is that the first tween gets a delay of 1 second added to its parameters, while the second tween should not have the delay.
Have you already spotted my blunder? If so, go check out the TweenLite class and have fun – I have nothing to teach you here (where were you when I needed you!?!). If you, like I, naively think that this code above will work as intended, read on. Read and learn, my friend; read and learn.
What actually happened is that the second tween gets the delay as well. This had me really stumped, as we’re dealing with two completely unrelated variables (paramsOne and paramsTwo) that both have their own scope. There should be no collision between the two so why the hell was TweenLite still keeping the delay:1 property? Naturally I blamed TweenLite – somehow it was clearly cacheing the delay property. So I dug through the documentation and eventually the source code to try to figure out where or why this cacheing was occurring and how to override it.
But the problem wasn’t with TweenLite at all.
Modifying soft copies modifies the original
The two variables paramsOne and paramsTwo both took soft copies of the original tweenParams object. Soft copies are not completely independent objects – they are really just “aliases” (pointers, in C-speak) to the original object. The real gotcha here is that if you make a change to the soft copy, you’re also making a change to the original object! So when I add
paramsOne.delay = 1;
I’m really saying:
tweenParams.delay = 1;
because paramsOne is nothing more than an alias of tweenParams. This is actually really dangerous when forgotten or ignored. Case in point:
paramsTwo:Object = tweenParams;
now paramsTwo has a “delay” property too because tweenParams has a “delay” property.
Bloody hell.
Shallow copies – cloning an object’s properties
[Insert bad StarWars joke here related to Clones]
Cloning an object means returning a copy of the object that has no more “ties” to the source object. Modifying the clone will in no way impact the source object. You can do a “shallow copy” of an object fairly easily if you know the type, by simply iterating over each property of the object and assigning each value to a corresponding property in a newly created object. So rather than
objB:Object = objA;
it’s:
objB:Object = new Object(); objB.propertyOne = objA.propertyOne; objB.propertyTwo = objA.propertyTwo;
…and so on. Obviously you can use any of ActionScript’s built-in iterators (for..in, for example) to loop through object A, grab its properties and apply them to the new object. If you encapsulate this in some kind of function or class, you’ve got yourself a useful utility that can be deployed at need.
function shallowCopy(sourceObj:Object):Object {
var copyObj:Object = new Object();
for (var i in sourceObj){
copyObj[i] = sourceObj[i];
}
return copyObj;
}
The limitation of this method (and why it’s called a “shallow copy”) is that if any of the properties we are copying are, themselves, objects with their own properties (nested objects), we’ll only be getting references to those nested objects – the very same problem that got us into this hot water to begin with.
So, the shallow copy method, while intuitive, is really only effective when you’re dealing with objects whose property lists are 1-dimensional, or where you really only need to worry about modifying the top level properties of the object. At the end of the day, you just have to be aware of what you’re copying and what your taking a reference to, and you’l be [in the words of CSI's Horatio] … just fine.
Cloning and deep copies
For some reason I had heard the term “deep copy” floating about and knew it was related to the issue at hand. A quick Google search yielded many interesting posts – I highly recommend you educate yourself on the more intricate methods. What follows here is just the most rudimentary way to clone an object.
Essentially a Deep Copy refers to recursing through the object’s properties, and any sub-properties of those properties, and perfoming clone operations at every level. This ensures that if an object’s property is, itself, some kind of object, that object is cloned as well, leaving absolutely no ties whatsoever to the original object(s).
There are numerous techniques, each with their own advantages and limitations, but this one seems to be fairly widespread, is used internally in Flex’ built-in ObjectUtil.copy() method. If you’re using Flash, not Flex, you’ll need to define this one yourself:
private function clone(obj:Object):Object {
var temp:ByteArray = new ByteArray();
temp.writeObject(obj);
temp.position = 0;
return temp.readObject();
}
And you can use it like this:
objB:Object = clone(objA);
Now of course, there are subtleties and implications here, that are beyond the scope of this blog, especially when cloning custom classes and various DisplayObject types, but for the applications I have been describing using generic value / data objects, this technique will work like a charm. I’ll let you read up on it if you want to understand what the hell a ByteArray is and why it works.
Read-only data objects
So, after edumacating myself on all this, it occured to me that for what I was doing, all this talk of shallow and deep copies and cloning just seemed a little overkill. Really what I needed was a way to define a static “data object” to contain my original parameter data in an immutable (unmodifiable) state, and to then be able to “tear off” copies of that original “read only” object as a starting point for new, temporary value objects that would power the parameters that I was passing to TweenLite.
The first solution that presented itself was to create and actual, external, stand-alone Class for the value object. Because I would get a new copy automatically whenever I invoked the class via its constructor (var my paramsOne:TweenParams = new TweenParams(); ), there would be no worries of every having any collisions between variables and the original values would remain untouched. This is probably the “correct” way to go, but seemed like a lot of overkill just to pull a couple parameters off. Further, it didn’t help achieve my overall goal which was to make my code more easy to read and easy to modify, because now you have to go hunting for yet another tiny trivial class to figure out how to go in and modify just a handful of simple Tween properties.
But I was able to take that same principle, and encapsulate it locally into an anonymous function that would just return the values I wanted, no strings attached. Basically like a stand-alone getter method, without the rest of the OO “wrapper”. This technique creates a simple function that returns the data object anonymously, so there’s no reference to the original object. In fact, there is really no original object, so there’s no chance of collision or references:
var getTweenParams:Function = new function():Object { return {
x: 500,
y: 400,
alpha: .5
}};
so getTweenParams is actually a Function, not an Object, so it cannot be modified when we alter the resulting object, like so:
var paramsOne:Object = getTweenParams();
And there you have it. I hope this was elucidating and clear enough to understand. Below are some examples that you can basically copy and paste and test in the FlashIDE to see some of these principles in action.
// this shows the dangers of soft references, and shows a simplified clone() function that leverages the ByteArray class
package {
import flash.display.MovieClip;
import flash.utils.ByteArray;
public class FunctionScope extends MovieClip {
private static const sourceObj:Object = {
a: "Aye",
b: "Bee"
}
public function FunctionScope(){
functionOne();
functionTwo();
traceObj(sourceObj);
functionThree();
traceObj(sourceObj);
}
private function functionOne():void {
var myObj:Object = sourceObj;
myObj.c = "Cee";
traceObj(myObj);
}
private function functionTwo():void {
var myObj:Object = sourceObj;
myObj.d = "Dee";
traceObj(myObj);
}
private function functionThree():void {
var myObj:Object = clone(sourceObj);
myObj.e = "Eew";
traceObj(myObj);
}
private function traceObj(obj:Object):void {
trace("Tracing: " + obj);
for (var i in obj){
trace(i + ":" + obj[i]);
}
}
private function clone(obj:Object):Object {
var temp:ByteArray = new ByteArray();
temp.writeObject(obj);
temp.position = 0;
return temp.readObject();
}
}
}
// this one shows the read-only object principle and illustrates that it works as claimed:
package {
import flash.display.MovieClip;
public class ObjectReadOnly extends MovieClip {
private var getReadOnlyValues:Function = function():Object { return {
a: "Aye",
b: "Bee",
c: "Cee"
}};
public function ObjectReadOnly () {
var copyOne:Object = getReadOnlyValues();
copyOne.d = "Dee";
var copyTwo:Object = getReadOnlyValues();
copyTwo.e = "Eew";
traceObj(copyOne);
traceObj(copyTwo);
traceObj(getReadOnlyValues());
}
private function traceObj(obj:Object):void {
trace("Tracing: " + obj);
for (var i in obj){
trace(i + ":" + obj[i]);
}
}
}
}