Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

Here, we collect all ActionScript 3 language features that are not present in ECMAScript 3 and document how we are going to simulate them.

General assumptions

We use

  • ECMAScript 5 strict mode whenever possible
  • ECMAScript 5 API (Object.create() etc.) that we know can be "polyfilled" in IE8 adequately
  • a well-known module / dependency mechanism like CommonJS modules or AMD
  • no other JavaScript library like jQuery, but "VanillaJS"

AS3 language features missing in ECMAScript 3

For every non-trivial language feature, we should introduce a dedicated child page.

Compilation Units

An ActionScript application consists of several compilation units, where one is selected as the application's entry point. A compilation unit is defined in one ActionScript source file, containing a primary declaration and optionally static code. Compilation units are organized in packages.

Types of Compilation Units

Primary declarations can be of the following types:

  • class – the most common case is that a source file defines one (externally visible) class.
  • interface – in ActionScript, a compilation unit defining an interface may not contain any executable code, so this is a special case handled in section "Interfaces and as/is operator".
  • function – for example in the Flash API, there are several functions defined in their own compilation unit. While this feature is less known, it is possible to define your custom package-scope functions.
  • var or const – Even variables or constants can be declared outside a class in their own compilation unit.

Property (get/set function) do not seem to be supported as compilation units.

Static code

Any ActionScript code in a compilation unit that is not part of a class member declaration belongs to this compilation unit's static code.
Static fields may also have initializers which are quite similar to static code.
Static code and static initializers of a compilation unit are executed exactly once, the first time code accesses its primary declaration. Note that this has nothing to do with import directives, nor does a compilation unit trigger immediate initialization of all compilation units to which it has static dependencies.

No matter how compilation unit initialization is triggered, it is always executed as a whole:

  1. first, the compilation unit containing the superclass of its primary declaration is initialized (which initializes its "super compilation unit" transitively)
  2. then, all static initializers are evaluated, in the order in which they appear in the source code
  3. last, all static code statements are executed, in the order in which they appear in the source code

Note that 1. and 2. only apply to compilation units whose primary declaration is a class.

Circular Dependencies

Compilation units are allowed to have circular dependencies (e.g. class A uses a constant from class B and class B calls a static method of class A).
This is important for our JavaScript solution, as it means that a compilation unit reference must be available early, to allow mutual references between compilation units.

Packages

In ActionScript, packages do not exist at run-time, instead they lead to primary declarations having a fully qualified name to avoid name clashes of global identifiers.
Mapping compilation units to AMD modules, it is straight-forward to define a primary declaration that is inside a package as a module with the fully qualified name of that class. AMD modules usually use nested directories and thus slashes ("/") instead of dots.

Looking at how classes are usually used in ActionScript, it turns out that they have to be imported (in contrast to Java, even if they are used by their fully-qualified names in the code!) and are only used by their fully-qualified names in the rare case of local name clashes.
When a module with a fully qualified name / path is required (loaded), it is assigned to a callback parameter, so that there is no need to use the fully qualified name. Only on name-clashes between un-qualified names, the parameter has to be renamed, for example by using the fully-qualified names with "." replaced by "$".

Implementation Solution

For our implementation, we have to take into account that in a browser, code loading cannot be done synchronously "on demand". Thus we use AMD and map compilation units to AMD modules. The AMD loader resolves all static dependencies and loads the code of all compilation units that are potentially needed at runtime. We have to take care of initializing those compilation units only when they are actually used. This determines the value of a compilation unit the corresponding AMD module returns: Instead of the primary declaration itself, the return value is a compilation unit object from which you can retrieve the primary declaration. The trick is that the compilation unit self-initializes when the primary declaration is first requested.

We could implement the primary declaration request by a method of the compilation unit object. But this would involve a function call for each retrieval of the primary declaration, even if the compilation has already been initialized. A trick for an efficient implementation is to use a get property that, upon its first invocation, initializes the compilation unit and replaces itself by a simple field with a direct reference to the primary declaration. For the sake of brevity and readability (we want the reader to mainly ignore this helper property), let's call that property _ (underscore).

Following from circular dependencies being allowed (see above), we have to use a certain AMD format, where the module value is an object created and handed-in by the module loader (usually called exports, but to avoid name clashes, we call it $exports), which is then populated by the module code.

Example

Here is a compilation unit with an ActionScript class with a reference to another class:

compilation unit com/acme/ThisClass.as:

Code Block
package com.acme {
import com.acme.OtherClass;

public class ThisClass {
  public function ThisClass() {
    OtherClass.doSomething();
  }
  ...
}

trace("ThisClass is being initialized...");

}

would in generated JavaScript become

Code Block
define("com/acme/ThisClass", ["exports", "com/acme/OtherClass"], function($exports, OtherClass) {
  Object.defineProperty($exports, "_", {
    configurable: true, // so we can replace it later!
    get: function() {
      function ThisClass() {
        OtherClass._.doSomething();
      }
      // replace "_" by simple property, returning the class:
      Object.defineProperty(this, "_", { value: ThisClass });
      ...
      trace("ThisClass is being initialized...");
      return ThisClass;
    }
  });
});

The first argument to define can be left out when the definition is the only one contained in the file, then the module name is derived from the file name.

Note how the definition of ThisClass, instead of directly returning its constructor function, exports a get property _, and how its implementation replaces that property by a read-only reference to the constructor (that's why it has to be configurable first).
Also note that we use OtherClass as a reference to the compilation unit containing OtherClass as its primary declaration, and so have to rewrite normal access to the class by OtherClass._.

Non-ES5 Browser Issues

Since for the time being, we still target Internet Explorer 8 (or even 7), we need a solution that also works without get properties, so we need a more sophisticated solution for the _ property.
In order not to always need to perform a function call (which results in a runtime penalty and is distracting during debugging), we try to read the field first and only call the function in case the property is not yet defined. Following the general pattern of naming explicit get functions (see below), the factory function for IE8 would be called _$get, resulting in the following expression to access a class from potentially not-yet-initialized compilation unit: (OtherClass_._ || OtherClass_._$get())

Conclusion

Granted, simulating compilation units and exact static code execution makes the solution a bit more complex, but it is an important AS3 language feature, and using the property access trick makes the implementation quite efficient, at least in modern browsers. In many cases, creating classes only when they are actually needed even increases performance, at least application start-up time, but also when many static dependencies are not actually needed at runtime at all.

Classes

Like most JavaScript frameworks, we simulate classes by a constructor function and its prototype chain.
The ES5 API Object.create(prototype,propertyDescriptors) serves this task very well.
While members are properties of the constructor function prototype, static members are properties of the constructor function itself.
Non-public members are discussed in a dedicated sub-section.

Class structure

The translation pattern is a follows:

Code Block
public class Foo extends Bar {
  // constructor
  public function Foo(/*constructor parameters*/) {
    // constructor code
  }
  // member declarations
  ...
  // static member declarations
  ...
}

becomes

Code Block
function Foo(/*constructor parameters*/) {
  // constructor code
}
Object.defineProperties(Foo, {
  ... // rewritten static member declarations
});
Foo.prototype = Object.create(Bar.prototype, {
  constructor: { value: Foo },
  ... // rewritten member declarations
});

The constructor property has to be defined explicitly, as otherwise, a subclass would inherit that property from its superclass, resulting in the subclass having the same constructor as its superclass.

Members and visibility (public, protected, internal, private)

Only public and protected members should be stored as properties of the constructor function / its prototype as-is. For protected members, access rights are checked by the compiler.
For private and internal members, we have to avoid name-clashes. There are different cases:

  • Private methods (non-static as well as static ones) and private static fields are declared in a new scope, shared by all class members. Thus, they are visible for every method, but not from outside. Non-static private method calls have to be rewritten to run with the correct this.
  • Private non-static fields and internal members are renamed: they are post-fixed by $ plus the inheritance level of the declaring class, so that they cannot name-clash with fields of the same name, declared in a subclass or superclass. The inheritance level is a number starting at 0 for Object, continuing with 1 for each direct subclass of Object, then 2 for classes inheriting from these classes, and so on. So for example a private field foo in a class extending Object would be renamed to foo$1. Internal members are also renamed, because they could name-clash with members of the same name, declared in a subclass or superclass residing in another package.

For private members, see blog posts Simulating ActionScript in JavaScript: Private Members and its follow-up.

Interfaces plus "is" and "as" operator

In ActionScript, interfaces are mainly used by the compiler for type checking. However, type checks can also be performed at runtime, using the AS3 operators instanceof, is, and as. instanceof only works for classes and automatically has the correct semantics when simulating classes via the prototype chain, so it can be used as-is in JavaScript.
x as T is simply defined as x is T ? x : null and would be implemented as a runtime helper function.
So the only operator we really have to simulate is is. To do so, we need some information about interfaces:

  • for a class, the set of interfaces it implements
  • for an interface, the set of interfaces it extends

For easy on-demand loading of the second information, we should implement interfaces as AMD modules, too. The AMD value of an interface could simply be set set of extended interfaces, but I chose to use a function that receives an object and sets the names of all extended interfaces as keys in that object (the values do not matter, they are simply true). This implementation makes it easy and efficient to unite interfaces: simply apply all interface functions on an empty object.
A class definition requires all AMD modules of interfaces it implements and can thus easily create the set of all interfaces it (transitively) implements. This set is stored as a static meta property, let's call it $implements.
It is convenient to have a check whether a given object implements an interface as a method isInstance(object) each interface provides. This method has to check whether the given object's constructor (its class) has a $implements property, and if so, whether the interface's name is contained in $implements:

Code Block
function isInstance(object) {
  return object !== null && typeof object === "object" &&
         !!object.constructor.$implements &&
         fullyQualifiedName in object.constructor.$implements;
}

where fullyQualifiedName is the fully qualified name of the interface.

The is function now has to tackle some special cases, then check instanceof, and finally check whether T is an interface, and if so, call its isInstance() method:

Code Block
function is(object, type) {
  return !!type && object !== undefined && object !== null &&
         (object instanceof type ||
          typeof type.isInstance === "function" && type.isInstance(object));
}

Parameter default values

See blog post Simulating ActionScript Parameter Default Values in JavaScript.

Rest (...) parameter

See blog post Simulating ActionScript Rest Parameter in JavaScript.

Bound methods

In contrast to JavaScript, in ActionScript, when a method is not instantly invoked, but instead used like a value, it stays bound to its original object.
This is best shown in an example:

JavaScript:

Code Block
>>> o = { foo: "foo", method: function() { return this.foo; } }
[object Object]
>>> o.method()
"foo"
>>> f = o.method
function()
>>> f()
undefined

Actually, in the last call, this references the global object, usually window.

ActionScript 3:

Code Block
public class BindTest {
  public var foo:String = "foo";
  public function method() { return this.foo; }
}

>>> o = new BindTest();
[object BindTest]
>>> o.method()
"foo"
>>> f = o.method
function()
>>> f()
"foo"

To simulate this behavior in JavaScript, there is the ES5 API Function#bind(), which can easily be polyfilled for IE8.
JavaScript:

Code Block
>>> f = o.method.bind(o)
function()
>>> f()
"foo"

One important thing to note is that bind() creates a new function object every time it is invoked:

Code Block
>>> o.method.bind(o) === o.method.bind(o)
false

This is not only a performance / memory consumption problem, as in AS3, the method keeps its identity. This is important for the typical use case that bound methods are registered as event listener callbacks. When trying to remove an event listener, identity has to be ensured, or the function will not be found and the event listener is not removed.

The following utility function "caches" the bound version of a method at the object and returns the cached version if already present:

Code Block
function bind(object, boundMethodName) {
  if (object.hasOwnProperty(boundMethodName)) {
    return object[boundMethodName];
  }
  var boundMethod = object[boundMethodName].bind(object);
  Object.defineProperty(object, boundMethodName, {
    value: boundMethod
  });
  return boundMethod;
}

This ensures that

Code Block
>>> bind(o, "method") === bind(o, "method")
true

All the compiler has to do is to rewrite all method accesses that are not part of an apply expression by a call to bind():

ActionScript 3:

Code Block
  var f:Function = o.method;

If method is a method of o, the code is rewritten to

JavaScript:

Code Block
  var f = bind(o, "method");

"this" is always in scope

In JavaScript, you always have to write this.member explicitly.
In ActionScript, this is automatically in scope in every non-static method.
This could be simulated by enclosing the method body in a with(this) block, but this solution is discouraged because with is considered evil, not supported in strict mode, prevents optimizations, and the scope sequence of parameters would be wrong.

Complement "this."

Instead, the compiler has to take care to complement "this." before non-static class members. Note that class members may be hidden by local variables and parameters, so "this." must not be complemented in such cases.

ActionScript:

Code Block
public class MyClass {
  public var id:String;
  public function MyClass(id:String) {
    this.id = id;
  }
  public function toString():String {
    return id;
  }
}

JavaScript:

Code Block
function MyClass(id) {
  this.id = id;
}
Object.defineProperties(MyClass.prototype, {
  id: { value: null, writable: true },
  toString: {
    value: function() {
      return this.id;
    }
  }
});

Note that the second occurrence of id in the constructor body is not prefixed by "this.", as it refers to the parameter.

"this" inside functions

An occurrence of this inside a function (in contrast to a method) does not refer to the "outer" this defined by the class context:

ActionScript:

Code Block
public class MyClass {
  public var prefix:String;
  public function MyClass(prefix:String) {
    this.prefix = prefix;
  }
  public function prefixAll(values:Array):Array {
    return values.map(function(s:String):String {
      return this.prefix + s; // "this" is not defined here as intended!
    });
  }
}

This code does not work as intended, because this inside functions is bound dynamically.
You can solve this problem by either using the second optional parameter thisObject of Array#map(), or by simply removing the this. qualifier from prefix. Let's do the latter:

Code Block
  public function prefixAll(values:Array):Array {
    return values.map(function(s:String):String {
      return prefix + s;
    });
  }

This works, because the anonymous function inside prefixAll is also in lexical scope of the outer this. Unfortunately, this fact is hidden at runtime, because the local this of the anonymous function hides the outer this. Thus, to be able to access the outer this in the generated JS code, we have to alias it, say to this$:

JavaScript:

Code Block
function MyClass(prefix) {
  this.prefix = prefix;
}
Object.defineProperties(MyClass.prototype, {
  "prefix": { value: null, writable: true },
  "prefixAll": {
    value: function(values) {
      var this$ = this;
      return values.map(function(s) {
        return this$.prefix + s;
      });
    }
  }
});

This aliasing should be done at most once right at the start of the method only if it contains at least one function that needs an outer this reference.

Statements

Almost all statements can be mapped one-to-one from ActionScript to JavaScript.
There are only two exceptions: for each and try... catch.

for each

Unlike the for... in loop, the for each... in loop has been introduced to JavaScript later, so to make sure the generated code runs in JavaScript 1.5 browsers, we have to simulate it.

Notation:

  • $0, $1 are auxiliary variables generated by the compiler, chosen to avoid name-clashes with any other identifiers in scope.
  • <lhs>, <rhs> are arbitrary complex ActionScript expressions
  • <block> is an ActionScript code block (the loop body)

Then,

Code Block
for each (<lhs> in <rhs>) {
  <block>
}

becomes

Code Block
var $1;
for (var $0 in ($1 = <rhs>)) {
  <lhs> = $1[$0];
  <block>
}

Note that if <block> is not enclosed by curly braces, these have to added, as we add a second statement to the loop body.

try... catch... finally

While JavaScript supports try... catch... finally statements, it (naturally) does not support multiple catch clauses, using different error types, like so:

ActionScript:

Code Block
try {
  ...
} catch (e1:ArgumentError) {
  // handle argument errors
} catch (e2:TypeError) {
  // handle type errors
}

Note that a catch clause implicitly declares its variable, scoped to the catch body only. The variables could have the same name and would not clash.
The AS3 semantics is that if the error is an ArgumentError, the first catch body is executed, if the error is a TypeError, the second catch body is executed, and on any other error, the error is not caught at all.

Thus, the corresponding JavaScript code needs to check the runtime type of the error variable:

JavaScript:

Code Block
try {
  ...
} catch (e) {
  if (is(e, ArgumentError)) {
    var e1 = e;
    // handle argument errors
  } else if (is(e, TypeError)) {
    var e2 = e;
    // handle type errors
  } else {
    throw e;
  }
}

Note that in case no type check matches, the error has to be re-thrown. This code can be omitted if the original code contains a catch clause with an untyped or * typed error variable.

Operators

Besides as and is, which are discussed in the Interfaces section, only two operators are known to be missing in JavaScript, namely short-circuit-and-assignment (&&=) and short-circuit-or-assignment (||=).

For both operators, the trick is that both the left-hand-side and right-hand-side expression are not evaluated too often. To achieve this, we introduce auxiliary variables like for for each. The code transformation examples are for &&= only, as ||= works analogously.

Simple Pattern

ActionScript:

Code Block
<identifier> &&= <rhs>

JavaScript:

Code Block
<identifier> = <identifier> && <rhs>

Complex Pattern

If the left-hand-side can be split into two expressions, where the first evaluates to some object and the second to some property, do so and rewrite:

ActionScript:

Code Block
<exp1>[<exp2>] &&= <rhs>

JavaScript:

Code Block
var $0, $1;
($0 = <exp1>)[$1 = <exp2>] = $0[$1] && <rhs>

Note that foo().y is the same as foo()['y'] and thus also matches this pattern.

Examples

Simple lhs

ActionScript:

Code Block
x &&= foo()

JavaScript:

Code Block
x = x && foo()

Complex lhs

Complex object

ActionScript:

Code Block
foo().y &&= bar()

JavaScript:

Code Block
var $0, $1;
($0  = foo())[$1 = 'y'] = $0[$1] && bar();

which of course could be simplified to

Code Block
var $0;
($0 = foo()).y = $0.y && bar()
Complex property

ActionScript:

Code Block
x[foo()] &&= bar()

JavaScript:

Code Block
var $0, $1;
($0 = x)[$1 = foo()] = $0[$1] && bar()

which of course could be simplified to

Code Block
var $1;
x[$1 = foo()] = x[$1] && bar()
Complex object and property

ActionScript:

Code Block
baz()[foo()] &&= bar()

JavaScript:

Code Block
var $0, $1;
($0 = baz())[$1 = foo()] = $0[$1] && bar()

which cannot be simplified.