Functions as Objects - part 2
In Functions as Objects - part 1 I discussed the basics of Function objects and showed some simple examples of using functions as objects. In todays article I will delve into actionscript Function objects, starting with scope and how to manage it as well as when you may need to. The two Function object methods, call and apply, will be discussed with example showing how they are both similar and different. The difference of the apply method will come out in some example of arguments, the one property of Function objects. Finally, we will work through an example showing many different ways you can use function objects within other functions and in the end come up with a very useful function. ...
First on the list of topics is scope. Let's start with what we are referring to when we use the term scope. In Object oriented programming you run into a special property called 'this'. The 'this' property is a reference to the current object. Some people even refer to scope as the this object. How does scope effect Function objects? That is best shown through an example.
For this example to work properly you will need to place a button component on the stage and give it an instance name of 'aButton'( no quotes ).
function scopeTest( ) { trace( "this = " + this ); } aButton.onRelease = scopeTest; trace( "calling scopeTest( )" ); scopeTest(); //trace( "calling scopeTest.call( aButton )" ); //scopeTest.call( aButton ); //trace( "calling scopeTest.apply( aButton )" ); //scopeTest.apply( aButton );
When you run this code your output window should come up and look like this:
calling scopeTest( ) this = _level0
And if you press ( and release ) the button( named 'aButton' ) it will look like this:
calling scopeTest( ) this = _level0 this = _level0.aButton
Let's see what is happening here. First off we declare and initialize a Function variable 'scopeTest'. Buttons have a variable 'onRelease' that is made to hold Function objects. We pointed aButton's 'onRelease' variable to scopeTest so it could be used to reference the same Function. Next, the we call scopeTest with scopeTest() and get the output: this = _level0. Because the 'onRelease' variable gets called when we release the button ( how ingenious! ), whenever that occurs we get the output: _level0.aButton. This shows how scope can affect the use of Functions. If you wanted the scopeTest function to use any data from the root MovieClip you may not get the correct results, though actionscript can be forgiving in some cases, and you could end up with unpredictable results. So just be clear what scope a function is being run in, if scope matters. In some cases it won't matter. Using the same setup as above plug in this code and test it.
function add2and3( ):Void { var a:Number = 2; var b:Number = 3; trace( a + " + " + b + " = " + ( a + b ) ); } aButton.onRelease = add2and3; trace( "Calling add2and3" ); add2and3();
Now things are different. Whether called from add2and3() or from releasing the button you results are the same. This is because the Function does not have to reference this, the properties of this or the methods of this. It should be noted that this is not an issue in Actionscript 3.0 since functions retain their scope, however I have not done much work in AS3 so I will leave that information for a later time.
Since scope can have such a large affect on Function objects, they would be worthless if you could not put ensure functions got called in the scope you desired. Function object methods to the rescue. The 'call' and 'apply' methods allow you to do just that. Let's start off with an example, in fact let's start with the first example. Uncomment out the last 4 lines and run it. Without pressing the button you should get this:
calling scopeTest( ) this = _level0 calling scopeTest.call( aButton ) this = _level0.aButton calling scopeTest.apply( aButton ) this = _level0.aButton
Our wishes have been granted. We can now call a function in whatever scope we want. Unfortunately we can't simply type:
aButton.onRelease = scopeTest.call( aButton )
because, as we learned from part 1, scopeTest.call( aButton ) is not a reference to a function. It is a function call, on the scopeTest.call function ( and thus represents Void, or nothing ). We'll get back to dealing with that later.
For now let's focus on 'call' and 'apply'. From the example above, it looks like they do the same thing. They do, the difference is in the parameters used when calling them. For both functions the first parameter is scope. For 'call' the parameters passed in after that are passed into the function you are calling. For 'apply' there is only a second parameter, an array of the parameters that you want to pass into the function you are calling. I will show the difference in an example that uses both of them to get the same results:
function add( a:Number, b:Number ):Void { trace( a + " + " + b + " = " + ( a + b ) ); } trace( "add.call( null, 2, 3 )" ); add.call( null, 2, 3 ); var numbers:Array = new Array( 2, 3 ); trace( "add.apply( null, numbers )" ); add.apply( null, numbers );
As you can see, 'call' is best used when you know the number of parameters that are needed and that are being applied and 'apply' is best used when you are working with an unknown number of parameters to be applied to a function.
A good place to find an unknown number of parameters for a function call is in a the 'arguments' property of Function objects. The FunctionArgument object 'arguments' is an object that uses array behaviour to get you a list of all the parameters passed into a function, even if they don't have a variable assigned to them. This point is demonstrated in this example:
function traceArgs( arg1, arg2 ):Void { trace( "arg1 = " + arg1 ); trace( "arg2 = " + arg2 ); for( var x in arguments ){ trace( "arguments[" + x + "] = " + arguments[x] ); } } traceArgs( "hello", "one", "goodbye", "fourth" );
From which you get
arg1 = hello arg2 = one arguments[3] = fourth arguments[2] = goodbye arguments[1] = one arguments[0] = hello
As you can see this functionality can be very helpful when you don't know how many parameters will be passed into a function.
Along with the power to see all passed in parameters, 'arguments' has 2 properties of its own, 'callee' and 'caller'. The property 'callee' is a reference to the currently executing function. It seems somewhat pointless, but we will be using this in our final example. The 'caller' property is a reference to the function that called the currently executing function. I have used the 'caller' property to take different actions depending on what was calling a function and also used it when working with super classes and static variables. However, 'caller' is often 'undefined' since many functions are not called from other functions and actually isn't even in actionscript 3.
Finally, how can I put all this information together from part 1 and part 2 to become a Function object power user. To be a Function object power user you should be able to use Function objects within Function objects. We are going to build a Function that takes a Function as a parameter and returns a function. The function we are going to build is somewhat complex so we will build and explain it line by line:
function createScopedFunction( scope:Object, method:Function, args:Array ):Function
The first line defines the Function that createScopedFunction will reference. It will take 3 parameters. The first parameter will represent scope( that's why I named it scope :) ) and can take any object. The second parameter, method, is the Function object we are passing in. The third is an Array we are passing in and we named it 'args'. The last part of the definition says that createScopedFunction will return a Function object. Now is a good time to start thinking about what we are trying to accomplish.
The next line looks like this:
var result:Function;
Simply put, we want to return a function so here we have created a variable that we will point to that function. We call that variable 'result'.
The next statement, which covers 3 lines:
result = function() { return arguments.callee.method.apply(arguments.callee.scope, arguments.concat(arguments.callee.args) ); };
In this code we are creating a Function object and initializing the variable 'result' with that Function. In breaking this down the first thing I want to talk about are the three "arguments.callee." references. Since arguments is inside the Function referenced 'result' and callee is a reference to the currently executing function. So when the function referenced at 'result' executes arguments.callee will reference the same thing as 'result'. So what about method, scope and args? We will get to that in a moment. For now see that we basically have a function body that looks like this:
return method.apply( scope, arguments.concat( args ) ). Now you should be able to see that we are going to use 'apply' to make a function call to 'method', using 'scope' as a this object and with parameters made up by concatenating 'args' to 'arguments' passed into 'result' and we are going to return the value we get from that function call. Is this starting to make sense?
The last five lines look like this:
result.scope = scope; result.method = method; result.args = args; return result; }
Remember arguments.callee.scope, arguments.callee.method and arguments.callee.args? Remember how we said in that instance that arguments.callee referenced the same thing as 'result'? The first 3 lines here add the scope, method and args properties to the Function referenced by 'result' and initialize them with the 'scope', 'method' and 'arg' parameters from the function declaration. We said we were going to return a Function object so the second to last line, "return result;", does just that. And finally we close our function statements. The finished code should look like this:
function createScopedFunction( scope:Object, method:Function, args:Array ):Function { var result:Function; result = function() { return arguments.callee.method.apply(arguments.callee.scope, arguments.concat(arguments.callee.args) ); }; result.scope = scope; result.method = method; result.args = args; return result; }
If you've ever heard of the Delegate class this is very similar to Delegate.create. It allows you to create a function that gives you the return from a function you call in a given scope. This version allows you the ability to also send parameters. To see it in action let's revamp our very first example so it uses our new Function:
function createScopedFunction( scope:Object, method:Function, args:Array ):Function { var result:Function; result = function() { return arguments.callee.method.apply(arguments.callee.scope, arguments.concat(arguments.callee.args) ); }; result.scope = scope; result.method = method; result.args = args; return result; } function scopeTest( ):Void { trace( "this = " + this ); } aButton.onRelease = createScopedFunction( this, scopeTest ); trace( "Calling scopeTest( );" ); scopeTest();
The results you see should be different than we originally achieved.
----
Daryl "Deacon" Ducharme is currently "Code Czar" for the Interactive Agency Provis Media Group, LLC which helps organizations enhance identity, connect with customers and increase productivity.