More Frontal Scripting
From Frontal Wiki
Contents |
Scope
Variables have a very curious and incredibly necessary quality of only being accessible when they share the same "scope" as the code that is accessing them. That is, there are ways to structure your code to effectively shut your variable into a room such that only scripts that are also in that room can see it and access it. This becomes important when scripts become more complex, or when you want to have variables that are unique to a particular Frontal element.
We use the "var" statement to keep a variable in the current scope (or "room" in our analogy). If we leave the "var" off, then we're adding the variable to the global scope, which can be seen by any script regardless of which room it's in. (Specifically, the global scope is specific to the current document. The Frontal renderer can support multiple documents at once, and each one gets its own global scope. This is discussed further under Even More Advanced Topics.) The last thing to remember is that interactions (scripts that are associated with attributes like "onClick" and "onRollOver") run in their own scope, whereas scripts that are included with the 'script' tag run in the global scope.
In this example, we show how the 'script' tag creates a variable in the global scope. Then we show how interactions have their own scope that can hide variables in the global scope, and can also access the global scope and add to it.
<script><![CDATA[ var msg = "Hello, world!"; // Create a variable in the global scope. msg2 = "Goodbye, Terra!"; // Also in the global scope. ]]></script> <style><![CDATA[ text { @onClick { // Create a variable in our local scope. This does not affect the "msg" variable in the global // scope, it just changes what msg refers to in this scope. var msg = "Bonjour, monde!"; // This writes "Bonjour, monde!" because in our scope that's what we've set msg to be. document.write ( msg ); // This writes "Goodbye, Terra!" because we don't have a msg2 in our scope and so the // interpreter looks for it and finds it in the global scope. document.write ( msg2 ); // Because we've left off the "var" operator, this creates the "msg3" variable in the global // scope. msg3 = "Greetings, Mars!"; // We're about to leave this scope, which will cause it to be thrown away. At the next // onClick event, we'll start over with a new scope. } @onRollOver { // This always writes "Hello, world!" because we're running in a new scope unrelated to // any scope used during the onClick interaction. document.write ( msg ); // This won't write anything until we click on the text, which will create the msg3 variable // in the global scope. document.write ( msg3 ); } } ]]></style> <text>click me</text>
So if we run this example in the Frontal Workspace, and then roll over the "click me" text, we get this output:
Hello, world!
Then if we click the text, we get this:
Bonjour, monde! Goodbye, Terra!
And then if we roll off and back over the text, we get this:
Hello, world! Greetings, Mars!
Why is scope so important? It's the mechanism by which we control sharing and privacy of variables (and as we'll see later, functions) within our scripts. As things get more complex, or simple but widely used, this mechanism becomes vital.
What is 'this' and What is 'this.dynamic'?
Frontal's scripting language, like JavaScript, has a special variable called 'this'. The concept is that whenever a script is running, it's associated with exactly one object that is 'this'. For example, when a 'script' tag is being interpreted, 'this' points to the Frontal document. And when an interaction is being interpreted, 'this' points to the particular tag that is being effected by the visitor's action.
This is very helpful, as it allows a script to act in context. That is, the same script can be applied to different elements and can act on those different elements uniquely. For example, here we have two toggle buttons that act separately despite the fact that they share the same code.
<style><![CDATA[ text { @onClick { this.dynamic.clicked = ! this.dynamic.clicked; var color = "black"; if ( this.dynamic.clicked ) color = "red"; sS ( "color", color ); } } ]]></style> <text>toggle one</text> <text>toggle two</text>
Notice how we used the 'this' variable to keep track of the toggle's "clicked" state on the particular toggle that was being clicked. If we naively used a global variable, then the two toggles would interact with one another and not give us the behavior we want.
In this example, you see that we used the special variable 'this' along with another not-so-special-but-always-present variable named 'dynamic'. This is because in Frontal (unlike JavaScript), we're not allowed to add properties to the 'this' object. That is, in JavaScript, we could've written "this.clicked" directly to track the toggle's state, but in Frontal, this isn't allowed. The reason for this is to avoid conflicts with future additions and features. So as a remedy, all Frontal document elements have a special variable called 'dynamic' upon which properties may be added at will, as we've done here with the property 'clicked'.
One final note on 'this' -- we don't need to use it if it can be inferred. That is, if a script refers to a property on the 'this' object and there's no variable of the same name defined in the current scope, then 'this' may be omitted. So in the example above, we could've written "dynamic.clicked" instead of "this.dynamic.clicked". This wouldn't have been the case, however, if we'd also declared a variable named 'dynamic' in that interaction.
Closures
When we define a function in Frontal, it has what's known as "closure". What this means is that all of the variables that are in scope when the function is defined are still in scope when the function is called, even though the defining code may have completed long ago.
From the previous section #Scope, we know that we can use the "var" statement to keep the scope of a variable confined to some part of our code. This allows us to define some variable, say "x", to have one meaning in one part of our code and another meaning in another part.
<script><![CDATA[ dynamic.x = 5; document.write ( "x = " + dynamic.x ); ]]></script> <script><![CDATA[ dynamic.x = "five"; document.write ( "x = " + dynamic.x ); ]]></script>
Now, let's place the calls to write out the value of "x" into two functions: "writeX1" and "writeX2". And let's add a third script tag that calls each of these functions as well as prints out "x" for itself. Paste this into the Workspace and see what you get.
<script><![CDATA[ dynamic.x = 5; function writeX1 ( ) { document.write ( "x = " + dynamic.x ); } ]]></script> <script><![CDATA[ dynamic.x = "five"; function writeX2 ( ) { document.write ( "x = " + dynamic.x ); } ]]></script> <script><![CDATA[ writeX1 ( ); writeX2 ( ); document.write ( "x = " + dynamic.x ); ]]></script>
writeX1 and writeX2 remember their local value of x even though they're no longer in that scope! This is what closure is, and it can be very handy to associate a function with some context, like in the above example, a particular 'script' element.
But what if you have a very useful function that you want to be able to apply to any scope, and not just the one it was defined in? Of course, one way, and often the best way, is to define those functions to work on parameters, and simply ignore their scope. That is, if we want writeX1 to write the value of the local x variable, then just pass it in as a parameter:
<script><![CDATA[ dynamic.x = 5; function writeX1 ( x ) { document.write ( "x = " + x ); } ]]></script> <script><![CDATA[ writeX1 ( "five" ); ]]></script>
In this example, writeX1 has closure, and if asked to write the value of dynamic.x, it'd show 5, but it's not using its closure -- it's being passed in as a parameter instead. So we've worked around the issue of a clingy closure.
But that's not always possible. For example, sometimes we want to apply a function to some object and thus use that object as its scope. When we have closure, it can get in the way.
Here's an example of that. We define writeX1 such that its closure says dynamic.x is 5. We then apply the function to another script object in which dynamic.x is "five". What's printed out though is still 5. We'd like it to print "five". (The "apply()" method is part of the Flash Function class. See the Flash documentation for more details.)
<script><![CDATA[ dynamic.x = 5; function writeX1 ( ) { document.write ( "x = " + dynamic.x ); } ]]></script> <script><![CDATA[ dynamic.x = "five"; writeX1.apply ( this ); ]]></script>
To correct this, we need to remove the closure from the writeX1 function. The closure is stored in a few member variables of the function itself and we remove or overwrite them.
For example, here we are clearing all closure information for writeX1 just after we define it:
<script><![CDATA[ dynamic.x = 5; function writeX1 ( ) { document.write ( "x = " + dynamic.x ); } delete writeX1.theThis; writeX1.executionContexts = [ ]; writeX1.scopeChain = [ ]; ]]></script> <script><![CDATA[ dynamic.x = "five"; writeX1.apply ( this ); ]]></script>
And now the example displays "five" as we wished.
Using the Prototype Property to Define Classes
The scripting language in Frontal allows you to define classes with inheritance much as you would with JavaScript. Frontal extends this to also automatically support closures on methods in such classes. This functionality allows you to develop class libraries for the Frontal language that can greatly simplify and speed up your development. For example, we've provided a basic XML RPC library (http://frontalcode.com/assets/xml/xmlrpc.xml.) to do things like integrate with Flickr's APIs to retrieve images and conduct searches. The library was implemented with this technique.
Defining a Class
The basic form of defining a new class with this technique is to create an empty function with the name of the class:
function MyClass ( ) { }
Internally, this creates a Function object with a special member called "prototype" that is an empty object (associative array). It's with the prototype object that we define members and methods for this class.
So the next step is to define the class's member variables. This is done directly on the prototype object:
MyClass.prototype.title = "Hello, I'm from MyClass."; MyClass.prototype.x = 0;
And we can also define methods:
MyClass.prototype.getTitle = function ( ) { x++; return title + " " + x; } MyClass.prototype.showTitle = function ( ) { com.frontalcode.Debugger.msg ( getTitle ( ) ); }
We can use our class with the "new" operator. Paste the code we've written so far along with a 'script' tag into the Frontal Workspace:
<script><![CDATA[ /* Paste our class code here */ var a = new MyClass ( ); a.showTitle ( ); // Displays "Hello, I'm from MyClass. 1" com.frontalcode.Debugger.msg ( a.x ); // Displays 1 since x was incremented in the call to getTitle. var b = new MyClass ( ); com.frontalcode.Debugger.msg ( b.x ); // Displays 0 since this instance of MyClass has not called getTitle. ]]></script>
Extending a Class
We subclass our class in the following way:
function YourClass ( initY ) { y = initY; } YourClass.prototype = new MyClass ( ); YourClass.prototype.constructor = YourClass; YourClass.prototype.y = 0; YourClass.prototype.getTitle = function ( ) { y++; // It's awkward, but this is how we call a method in our super class: return MyClass.prototype.getTitle.apply ( this, arguments ) + " By way of YourClass. " + y; }
Try it out. Paste the above along with the following code, beneath what we had before.
var c = new YourClass ( 5 ); c.showTitle ( ); // Displays "Hello, I'm from MyClass. 1 By way of YourClass. 6" com.frontalcode.Debugger.msg ( c.x ); // Displays 1 since x was incremented in the base class's getTitle method. com.frontalcode.Debugger.msg ( c.y ); // Displays 6 since y was initialized to 5 and was incremented in the call to getTitle.
Closures on Methods
Unlike JavaScript, in Frontal, an object's methods created this way have closure, which allows them to run in the scope of the object, even if only referred to by a function pointer. So for example, if we call a method of the instance in an event listener, it'll know which instance it's associated with. For example:
<text id="test">click me 1</text> <text id="test2">click me 2</text> <script><![CDATA[ gE ( "test" ).movie.addEventListener ( flash.events.MouseEvent.CLICK, a.getTitle ); gE ( "test2" ).movie.addEventListener ( flash.events.MouseEvent.CLICK, b.getTitle ); ]]></script>
If we click "click me 1", then "Hello, I'm from MyClass. 2" is displayed. The "2" indicates that we're indeed getting a callback on the getTitle method of the object "a". And if we click "click me 2", then "Hello, I'm from MyClass. 1" is displayed confirming this.