Hack 77 Clone an Object

figs/moderate.gif figs/hack77.gif

ActionScript copies objects by reference rather than by value. Clone an object to create an independent copy rather than a reference to the original.

In many situations, you will store values as properties of a single object, instead of as separate variables, because the first technique offers a cleaner, more structured approach. However objects are not always treated the same way as simple variables. Consider the following code:

var x = 5;

var y = x;

trace(y);   // Displays: 5

x += 1;

trace(x);   // Displays: 6

trace(y);   // Displays: 5

We set x equal to 5, assign the value of x to y, and then increment x. After the example, the value of x is 6 and the value of y is 5. As expected, they are independent; changing the value of x after the assignment statement var y = x; doesn't alter the value of y. Therefore, variables that hold primitive values such as numbers are said to be passed by value because ActionScript copies the value held in variable x at the time it is assigned to the variable y.

Let's try the analogous operation with objects:

var obj1 = new Object( );

obj1.prop = 5;

var obj2 = obj1;

trace(obj2.prop);  // Displays: 5

obj1.prop += 1;

trace(obj1.prop);  // Displays: 6

trace(obj2.prop);  // Displays: 6

This time, we create object obj1, add property prop to it, and set obj2 equal to obj1. Do we have two separate objects? Immediately after the assignment, obj2.prop is 5, as we'd expect. However, if we increment obj1.prop and then trace the value of obj1.prop and obj2.prop, we find they are the same?they are both 6. Did you expect obj2.prop to still be 5 even after obj1.prop was set to 6?

The explanation is that we have not created two separate objects, but two references to a single object (both obj1 and obj2 hold a pointer to the same location of the object's data in memory, not a primitive datum such as the number 6).

Because both obj1.prop and obj2.prop point to the same thing in memory, changes made to the value via either reference are reflected in both. This is akin to the way that you can drag a single symbol to the Stage multiple times to create multiple movie clip instances. If you change the Library symbol of the movie clip, all instances reflect the change. (However, the analogy doesn't hold 100%. Movie clip instances, although dependent on the Library symbol, are independent of one another. Changes to the instance properties of one clip are not reflected in other clips derived from the same Library symbol.)

Complex datatypes such as objects and arrays are said to be passed by reference. Complex datatypes can be very large, so passing them by reference is more efficient. For example, to operate on an array, Flash need pass only a pointer to the array rather than all the data within it.

This can be a problem, however, if you want the duplicate to be a separate copy rather than a reference to the original. You could clone an object by defining each property explicitly:

var obj1 = new Object( );

obj1.prop1 = 5;

obj1.prop2 = 12;

var obj2 = new Object( );

obj2 .prop1 = obj1.prop1;

obj2 .prop2 = obj1.prop2;

trace(obj2.prop1);   // Displays: 5

obj1.prop1 += 1;

trace(obj1.prop1);   // Displays: 6

trace(obj2.prop1);   // Displays: 5 (not 6)

The two objects, obj1 and obj2, and their properties, such as obj1.prop1 and obj2.prop1, are now independent.

We can build our own method to create a unique copy of an object rather than a reference to it, by adding the following clone( ) method to the Object class. This is ActionScript 1.0 code, but it will work in ActionScript 2.0 as well.

Object.prototype.clone = function( ) {

  if ( (typeof this) != "object" || (this instanceof Button) || 

     (this instanceof TextField) ) {

    // Return the original reference if this is a movie clip,

    // button, or text field (i.e., don't clone these types).

    return this;

  }

  var c = new Object( );

  for (var n in this) {

    var o = this[n];

    if ( (typeof o) == "object" &&

          !(o instanceof Button || o instanceof TextField)) {

      // Property is itself an object, so clone it recursively

      c[n] = o.clone( );

    } else {

      // Properties that are primitives are copied by value. 

      // Otherwise, it's a reference to a function, MovieClip,

      // Button, or TextField, which we'll copy by reference

      // because we can't easily create an onscreen representation.

      c[n] = o;

    }

  }

  return c;

}

// Keep clone instance method from showing up in for...in loops, 

// and protect it from deletion:

ASSetPropFlags(Object.prototype,["clone"],5,1);

The preceding code adds a new Object.clone( ) method to the Object class. It also works with most classes that inherit from Object, as all ActionScript classes do. The preceding code does not, however, attempt to clone movie clips, because the built-in MovieClip.duplicateMovieClip( ) method can already create an independent copy of a clip. Although you could use duplicateMovieClip( ) within the preceding code to clone nested clips, typically you wouldn't want to duplicate a clip just because more than one object has a property that refers to it. Similarly, you won't ordinarily want to clone existing text fields or buttons. You're more likely to use, say, MovieClip.createTextField( ) to create a new text field rather than clone an existing one.

Given the preceding definition of the clone( ) method, the following snippet creates an independent copy (i.e., clone) of obj1 and stores a reference to the new, independent object in obj2:

obj2 = obj1.clone( );

Contrast the preceding with:

 obj2 = obj1;

which simply makes obj2 store a reference to the same object as obj1.

Notice that:

  • If the clone( ) method is invoked on one of the graphical classes (a movie clip, button, or text field), it returns a reference to the object instead of cloning it. If the original is a primitive object, such as a number, it returns the value.

  • Likewise, the clone( ) method creates references rather than clones when an object property is a primitive or an instance of one of the graphical classes.

  • Only enumerable properties are attached to the cloned object. Enumerable properties include those you would usually want to search through within a for...in loop (such as custom properties and methods created by the developer) but not preset properties that define how the class works internally or ActionScript methods.

  • We use the undocumented ASSetPropFlags( ) [Hack #82] to protect our new Object.clone( ) method from deletion and hide it from for...in loops.

You can test the clone( ) method as follows:

// Create an object containing a nested object for testing

obj = {name:"test", num:10, bool:true, 

  childObj:{childName:"original child name", childNum:5}, root:_root};

// Clone the object

cloneOfObj = obj.clone( );



// Change a value in the new object

cloneOfObj.childObj.childName = "changed child name";



// Check to see if the two values are independent (they are)

trace("obj.childObj.childName = " + obj.childObj.childName);

trace("cloneOfObj.childObj.childName = " + cloneOfObj.childObj.childName);

The preceding code demonstrates how both the clone object and its child object are independent of the original object or its child object. The clone( ) method saves you from having to copy properties and user-defined methods manually if you want a new copy of an object.

Cloning for Motion Graphics

Call me old-fashioned, but I prefer to look at data and its effects visually, so here's another example in which you can see the difference between data that has been cloned versus data copied by reference.

The following code creates an sData ("sprite data") object, which is used to control several semi-transparent circular clips to produce a particle effect. The effect works by creating many identical movie clips, each of which has an independent instance of sData on its timeline.

function animate( ):Void {

  this.sData = sData.clone( )

  this._x = this.sData.x;

  this._y = this.sData.y;

  this.lineStyle(10, 0x404040, 10);

  this.moveTo(-0.5, 0);

  this.lineTo(0, 0);

  this.onEnterFrame = function( ) {

    this.sData.x += Math.random( ) * this.sData.s - sData.s / 2;

    this.sData.y += Math.random( ) * this.sData.s - sData.s / 2;

    this._x = this.sData.x;

    this._y = this.sData.y;

  };

}

var sData:Object = new Object( );

sData.x = 275;

sData.y = 200;

sData.s = 4;



for (var i:Number = 0; i < 500; i++) {

  var dot:MovieClip = this.createEmptyMovieClip("dot" + i,

                      this.getNextHighestDepth( ));

  dot.onEnterFrame = animate;

}

Notice that the preceding code contains an example of a common and proper use of copying a movie clip by reference. The dot variable holds a reference to the movie clip instance doti. It always points to the movie clip created within the loop without making a clone of the clip.

Each clip is unique and maintains its independent properties, such as size and position. The code creates a simple Brownian motion (random particle movement) simulation, as seen in gases and in Figure 10-7. All 500 clips are independent.

Figure 10-7. Brownian motion simulated with 500 movie clips
figs/flhk_1007.gif


If you change the first line in the animate( ) function so that it copies by reference instead of cloning:

this.sData = sData;

you get a totally different result, as shown in Figure 10-8.

Figure 10-8. Failing to clone (separate) the sData results in a clumpy animation
figs/flhk_1008.gif


In the independent example, each clip has its own sData object with separate x and y coordinates, so the dots move independently. In the nonindependent version, a single sData object is used throughout (i.e., all the local versions of this.sData contain a reference back to the original, sData).

Therefore, changing the properties of sData via one reference changes the properties of every this.sData. Instead of a random distribution, you get a constrained squiggle shape that dances around the Stage. All the particles act as part of a single, larger graphic entity because their controlling data is now related; they have lost their individuality.

Final Thoughts

References to existing clips are often used in loops when you'd rather avoid referring to separate clips by separate names. For example, in the following loop, we want to refer to the current movie clip that was just created with createEmptyMovieClip( ). It is much easier to refer to the new clip using the reference myClip rather than write code that refers to the movie clips (clip0 to clip9) by their specific instance names.

for (i = 0; i < 10; i++) {

  var myClip:MovieClip = this.createEmptyMovieClip("clip" + i, i);

  // Code that uses current clip, myClip goes here

  myClip._rotation = 50;

}

In some cases, using a reference is typically an error on the programmer's part?she might want to create independent objects instead; the Object.clone( ) method presented here allows you to easily create independent objects. In motion graphics, swapping between independent (unique) data and references allows you to create vastly different final effects with very little effort.

?Grant Skinner