Previous: Constructor Implementation, Up: Class Module Design


B.1.4 Static Implementation

The decisions behind ease.js's static implementation were very difficult. More thought and time was spent on paper designing how the static implementation should be represented than most other features in the project. The reason for this is not because the concept of static members is complicated. Rather, it is due to limitations of pre-ECMAScript 5 engines.

B.1.4.1 How Static Members Are Supposed To Work

The first insight into the problems a static implementation would present was the concept itself. Take any common Object-Oriented language such as C++, Java, or even PHP. Static members are inherited by subtypes by reference. What does this mean? Consider two classes: Foo and SubFoo, the latter of which inherits from the former. Foo defines a static property count to be incremented each time the class is instantiated. The subtype SubFoo, when instantiated (assuming the constructor is not overridden), would increment that very same count. Therefore, we can represent this by stating that ‘Foo.count === SubFoo.count’. In the example below, we demonstrate this concept in pseudocode:

    let Foo = Class
        public static count = 0
    let SubFoo extend from Foo

    Foo.count = 5
    SubFoo.count === 5 // true
    SubFoo.count = 6
    Foo.count === 6 // true

Figure B.2: Representing static properties in pseudocode

As you may imagine, this is a problem. The above example does not look very JS-like. That is because it isn't. JS does not provide a means for variables to share references to the same primitive. In fact, even Objects are passed by value in the sense that, if the variable is reassigned, the other variable remains unaffected. The concept we are looking to support is similar to a pointer in C/C++, or a reference in PHP.

We have no such luxury.

B.1.4.2 Emulating References

Fortunately, ECMAScript 5 provides a means to emulate references – getters and setters. Taking a look at Figure B.2, we can clearly see that Foo and SubFoo are completely separate objects. They do not share any values by references. We shouldn't share primitives by reference even if we wanted to. This issue can be resolved by using getters/setters on SubFoo and forwarding gets/sets to the supertype:

    var obj1 = { val: 1 },
        obj2 = {
            get val()
            {
                return obj1.val;
            },

            set val( value )
            {
                obj1.val = value;
            },
        }
    ;

    obj2.val; // 1
    obj2.val = 5;
    obj1.val; // 5

    obj1.val = 6;
    obj2.val // 6

Figure B.3: Emulating references with getters/setters (proxy)

This comes with considerable overhead when compared to accessing the properties directly (in fact, at the time of writing this, V8 doesn't even attempt to optimize calls to getters/setters, so it is even slower than invoking accessor methods). That point aside, it works well and accomplishes what we need it to.

There's just one problem. This does not work in pre-ES5 environments! ease.js needs to support older environments, falling back to ensure that everything operates the same (even though features such as visibility aren't present).

This means that we cannot use this proxy implementation. It is used for visibility in class instances, but that is because a fallback is possible. It is not possible to provide a fallback that works with two separate objects. If there were, we wouldn't have this problem in the first place.

B.1.4.3 Deciding On a Compromise

A number of options were available regarding how static properties should be implemented. Methods are not a problem – they are only accessed by reference, never written to. Therefore, they can keep their convenient ‘Foo.method()’ syntax. Unfortunately, that cannot be the case for properties without the ability to implement a proxy through the use of getters/setters (which, as aforementioned, requires the services of ECMAScript 5, which is not available in older environments).

The choices were has follows:

  1. Add another object to be shared between classes (e.g. ‘Foo.$’).
  2. Do not inherit by reference. Each subtype would have their own distinct value.
  3. Access properties via an accessor method (e.g. ‘Foo.$('var')’), allowing us to properly proxy much like a getter/setter.

There are problems with all of the above options. The first option, which involves sharing an object, would cause awkward inheritance in the case of a fallback. Subtypes would set their static properties on the object, which would make that property available to the supertype! That is tolerable in the case of a fallback. However, the real problem lies in two other concepts: when a class has two subtypes that attempt to define a property with the same name, or when a subtype attempts to override a property. The former would cause both subtypes (which are entirely separate from one-another, with the exception of sharing the same parent) to share the same values, which is unacceptable. The latter case can be circumvented by simply preventing overriding of static properties, but the former just blows this idea out of the water entirely.

The second option is to not inherit by reference. This was the initial implementation (due to JavaScript limitations) until it was realized that this caused far too many inconsistencies between other Object-Oriented languages. There is no use in introducing a different implementation when we are attempting to mirror classic Object-Oriented principals to present a familiar paradigm to developers. Given this inconsistency alone, this option simply will not work.

The final option is to provide an accessor method, much like the style of jQuery. This would serve as an ugly alternative for getters/setters. It would operate as follows:

    // external
    Foo.$('var'); // getter
    Foo.$( 'var, 'foo' ); // setter

    // internal
    this.__self.$('var'); // getter
    this.__self.$( 'var', 'foo' ); // setter

Figure B.4: Accessor implementation for static properties

Obviously, this is highly inconsistent with the rest of the framework, which permits accessing properties in the conventional manner. However, this implementation does provide a number key benefits:

So, although the syntax is inconsistent with the rest of the framework, it does address all of our key requirements. This makes it a viable option for our implementation.

B.1.4.4 Appeasing ES5-Only Developers

There is another argument to be had. ease.js is designed to operate across all major browsers for all major versions, no matter how ridiculous (e.g. Internet Explorer 5.5), so long as it does not require unreasonable development effort. That is great and all, but what about those developers who are developing only for an ECMAScript 5 environment? This includes developers leveraging modern HTML 5 features and those using Node.js who do not intend to share code with pre-ES5 clients. Why should they suffer from an ugly, unnecessary syntax when a beautiful, natural [and elegant] implementation is available using proxies via getters/setters?

There are certainly two sides to this argument. On one hand, it is perfectly acceptable to request a natural syntax if it is supported. On the other hand, this introduces a number of problems:

Now, those arguing for the cleaner syntax can also argue that all newer environments moving forward will support the clean, ES5-only syntax, therefore it would be beneficial to have. Especially when used for web applications that can fall back to an entirely different implementation or refuse service entirely to older browsers. Why hold ease.js back for those stragglers if there's no intent on ever supporting them?

Both arguments are solid. Ultimately, ease.js will likely favor the argument of implementing the cleaner syntax by providing a runtime flag. If enabled, static members will be set using proxies. If not, it will fall back to the uglier implementation using the accessor method. If the environment doesn't support the flag when set, ease.js will throw an error and refuse to run, or will invoke a fallback specified by the developer to run an alternative code base that uses the portable, pre-ES5 syntax.

This decision will ultimately be made in the future. For the time being, ease.js will support and encourage use of the portable static property syntax.