ActionScript is unique among languages in that it is both static and dynamic. Although it is described as being "optionally statically typed," in practice you'll use static typing almost all the time, because that's what works best with Flex Builder, which can do code completion when it knows the types; you'll also get compile-time error checking and more efficient code generation.
However, whenever it suits your needs you can cross the line into dynamic typing, effectively turning off static type checking only for the small portions of your code where you want greater flexibility. This is strikingly powerful, because it means that when you need more dynamic code you aren't forced to jump through the hoops required by a static-only language. Basically, you get the best of both worlds.
In this article, I constantly need to display arrays in a meaningful fashion. I began exploring syntactically clean ways of doing this, and ended up learning about prototypes.
Here is a function for displaying arrays of any dimension, also placed in the com.mindviewinc library. For each element of the array, it checks to see if that element is also an array and if it is, makes a recursive call:
package com.mindviewinc.format {
public function formatArray(a:Array):String {
var result:String = ""
for(var i:int=0; i < a.length; i++) {
if(a[i] is Array)
result += formatArray(a[i])
else if(a[i] == null)
; // Do nothing
else
result += String(a[i])
result += ","
}
// Trim off last comma:
if(result.charAt(result.length - 1) == ',')
result = result.substring(0, result.length - 1)
return "[" + result + "]"
}
}
The is operator performs run-time type identification. If the element is an Array object, we make a recursive call; if it's uninitialized we do nothing (note the need for the semicolon in this case, to define an empty expression), otherwise we convert it to a string. In every case a comma is added, so if you have an array with a specified size that's uninitialized you'll still see a comma for each space.
If there's a trailing comma it's redundant and the Stringsubstring() method removes it. The return expression surrounds the result with square brackets.
If we use formatArray() with a TextDisplay object, the syntax for displaying an a:Array is:
t.show("a: " + formatArray(a))
Which is tolerable but tedious. It would be much nicer to be able to say:
a.show()
And have everything else taken care of for us. But this would appear to require us to add a new show() method to the Array class.
In the original JavaScript language, prototypes were the only inheritance mechanism available. Subsequently the more traditional inheritance mechanism has been added and for the most part prototypes are now rarely used, but they still have value because you can add methods dynamically to both objects and classes.
Ruby has the concept of open classes, where any existing class, even a builtin, can be "opened" for further modification. Prototypes are not quite that flexible; they are more like extension methods as found in C# 3.0. In particular, a method added via prototype can only access public elements of the class it's extending.
For example, suppose we start with this:
package com.mindviewinc.test {
public dynamic class TestClass {
internal var x:String = "howdy"
public var y:String = "Yall"
private function foo():String { return "Hello, foo" }
protected function bar():String { return "Hello, bar" }
public function baz():String { return "Hello, baz" }
}
}
The dynamic keyword is necessary if we wish to use prototypes on this class.
To use prototypes, we access the class' prototype field to assign new functions:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="TestPrototypes" xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:test="com.mindviewinc.test.*" xmlns:display="com.mindviewinc.display.*">
<display:TextDisplay id="t"/>
<mx:creationComplete>
import com.mindviewinc.test.TestClass
TestClass.prototype.f1 = function():String { return this.foo() }
TestClass.prototype.f2 = function():String { return this.bar() }
TestClass.prototype.f3 = function():String { return this.baz() }
TestClass.prototype.f4 = function():String { return this.x }
TestClass.prototype.f5 = function():String { return this.y }
var tc:TestClass = new TestClass()
// t.show("f1(): " + tc.f1()) // Runtime error: "foo is not a function"
// t.show("f2(): " + tc.f2()) // Runtime error: "bar is not a function"
t.show("f3(): " + tc.f3())
t.show("f4(): " + tc.f4())
t.show("f5(): " + tc.f5())
</mx:creationComplete>
</mx:Application>
The whole thing compiles even without the commented-out lines, but you get the runtime errors shown in the comments. For the function calls, the wording of the errors is not that helpful (it would be more valuable to say "not accessible" rather than "not a function"), but it gets the point across. Here's the output:
f3(): Hello, baz f4(): null f5(): Yall
Accessing the x and y fields is a different case. If a field is not public, that field will be created and given an initial value of null. So in that case you won't get a compiler warning or error, you just won't see the value you expect, so bugs are a little harder to track down.
We can now add the show() method to the existing Array class. And whenever this file is included, it will automatically create a TextDisplay named t:
//: Mindview/includes/show.as
// Include this to provide array.show()
import com.mindviewinc.display.TextDisplay
import com.mindviewinc.format.formatArray
var t:TextDisplay = new TextDisplay()
addChild(t)
Array.prototype.setName = function(name:String):void {
this.id = name
this.setPropertyIsEnumerable("id", false)
}
Array.prototype.show = function(msg:String=""):void {
if(msg.length > 0)
t.show(msg, " ")
if(this.hasOwnProperty("id"))
t.show(this.id, ": ")
t.show(formatArray(this))
}
Array.prototype.setPropertyIsEnumerable("setName", false)
Array.prototype.setPropertyIsEnumerable("show", false)
So we don't have to repeat the name of the array every time we display it, the first function attaches a name to the array as the field id. However, if you have an unnamed array, the test for hasOwnProperty() in show() won't try to print it.
By default, methods and fields that you add to an object are "enumerable," which means that a for each statement will produce them. In our Array case, when we call formatArray we only want to see the array elements. Attaching setName() and show() to Array using prototypes will make the new functions enumerable. So that none of these elements show up when displaying the array, we call setPropertyIsEnumerable() and make that property false.
To make this work, we must include this file rather than import it, so that the code is effectively pasted in and compiled as part of the file where it's been included (like a C language #include). I've named the above file show.as and placed it in the Mindview/includes directory. Here's an example where it's used:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="TestShowArray" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var a:Array = [1,2,3]
a.setName("a")
var b:Array = [[1,2,3],7,[4,5,6]]
b.setName("b")
var c:Array = [b, b]
c.setName("c")
a.show()
b.show()
c.show()
</mx:creationComplete>
</mx:Application>
Here, Array objects are created using the convenient square-bracket syntax. I've given them multiple dimensions in order to verify that formatArray() is working correctly. Here's the output:
a: [1,2,3] b: [[1,2,3],7,[4,5,6]] c: [[[1,2,3],7,[4,5,6]],[[1,2,3],7,[4,5,6]]]
Even though setName() and show() are not native methods of Array, their syntax makes them appear so. Although prototypes cannot access anything but public members of a class, which makes them much more limited than Ruby's open classes, they can come in handy when you only need to add a method or two in order to make an existing class callable by a new function.
While developing this article I ran into a problem when trying to create ActionScript functions (rather than classes) inside the Mindview library. I could place the file in the correct folder and give it the right package name, but Flex Builder would not recognize it and do completion, or even compile it if I typed in the import statement by hand.
This is clearly a bug in Flex Builder, but sometimes I could fix it by pretending to create a class in the library, then changing the contents inside the package to a function. Then it was recognized. Unfortunately, this didn't always work; sometimes the only way to make new library components visible is to start a new project after adding the library component. Ideally this bug will be fixed by the time or soon after you read this.