Previous: Visibility Object Implementation, Up: The Visibility Object


B.2.3.2 Property Proxies

Astute readers may notice that the visibility implementation described in the previous section (see Visibility Object Implementation) has one critical flaw stemming from how prototypes in JavaScript are implemented: setting a property on the visibility object bound to the method will set the property on that object, but not necessarily on its correct object. The following example will demonstrate this issue:

var pub = {
        foo:    'bar',
        method: function()
        {
            return 'baz';
        },
    },

    // what will become our visibility object
    priv = function() {}
;

// set up our visibility object's prototype chain (we're leaving the
// protected layer out of the equation)
priv.prototype = pub;

// create our visibility object
var vis = new priv();

// retrieving properties works fine, as do method invocations
vis.foo;       // "bar"
vis.method();  // "baz"

// but when it comes to setting values...
vis.foo = 'newval';

// ...we stop short
vis.foo;  // "newval"
pub.foo;  // "bar"

vis.foo = undefined;
vis.foo;  // undefined
delete vis.foo;
vis.foo;  // "bar"
pub.foo;  // "bar"

pub.foo = 'moo';
vis.foo; // "moo"

Figure B.16: Demonstrating property set issues with prototype chains

Retrieving property values and invoking methods are not a problem. This is because values further down the prototype chain peek through “holes” in objects further up the chain. Since vis in Figure B.16 has no value for property foo (note that a value of undefined is still a value), it looks at its prototype, pub, and finds the value there.

However, the story changes a bit when we try to set a value. When we assign a value to member foo of vis, we are in fact setting the property on vis itself, not pub. This fills that aforementioned “hole”, masking the value further down the prototype chain (our value in pub). This has the terrible consequence that if we were to set a public/protected property value from within a method, it would only be accessible from within that instance, for only that visibility object.

To summarize:

This issue is huge. Before ECMAScript 5, it may have been a show-stopper, preventing us from using a familiar this.prop syntax within classes and making the framework more of a mess than an elegant implementation. It is also likely that this is the reason that frameworks like ease.js did not yet exist; ECMAScript 5 and browsers that actually implement it are still relatively new.

Fortunately, ECMAScript 5 provides support for getters and setters. Using these, we can create a proxy from our visibility object to the appropriate members of the other layers (protected, public). Let us demonstrate this by building off of Figure B.16:

// proxy vis.foo to pub.foo using getters/setters
Object.defineProperty( vis, 'foo', {
    set: function( val )
    {
        pub.foo = val;
    },

    get: function()
    {
        return pub.foo;
    },
} );

vis.foo; // "moo"
pub.foo; // "moo"

vis.foo = "bar";
vis.foo; // "bar"
pub.foo; // "bar"

pub.foo = "changed";
vis.foo; // "changed"

Figure B.17: Using getters/setters to proxy values to the appropriate object

The implementation in Figure B.17 is precisely how ease.js implements and enforces the various levels of visibility.1 This is both fortunate and unfortunate; the project had been saved by getters/setters, but with a slight performance penalty. In order to implement this proxy, the following must be done:

Consequently, this means that accessing public properties from within the class will be slower than accessing the property outside of the class. Furthermore, accessing a protected property will always incur a performance hit3, because it is always hidden behind the provide object and it cannot be accessed from outside of the class. On the upside, accessing private members is fast (as in - “normal” speed). This has the benefit of encouraging proper OO practices by discouraging the use of public and protected properties. Note that methods, as they are not proxied, do not incur the same performance hit.

Given the above implementation details, it is clear that ease.js has been optimized for the most common use case, indicative of proper OO development - the access of private properties from within classes, for which there will be no performance penalty.


Footnotes

[1] One may wonder why we implemented a getter in Figure B.17 when we had no trouble retrieving the value to begin with. In defining a setter for foo on object vis, we filled that “hole”, preventing us from “seeing through” into the prototype (pub). Unfortunately, that means that we must use a getter in order to provide the illusion of the “hole”.

[2] One may also notice that we are not proxying public properties from the private member object to the public object. The reason for this is that getters/setters, being functions, are properly invoked when nestled within the prototype chain. The reader may then question why ease.js did not simply convert each property to a getter/setter, which would prevent the need for proxying. The reason for this was performance - with the current implementation, there is only a penalty for accessing public members from within an instance, for example. However, accessing public members outside of the class is as fast as normal property access. By converting all properties to getters/setters, we would cause a performance hit across the board, which is unnecessary.

[3] How much of a performance hit are we talking? This will depend on environment. In the case of v8 (Node.js is used to run the performance tests currently), getters/setters are not yet optimized (converted to machine code), so they are considerably more slow than direct property access.

For example: on one system using v8, reading public properties externally took only 0.0000000060s (direct access), whereas accessing the same property internally took 0.0000001120s (through the proxy), which is a significant (18.6x) slow-down. Run that test 500,000 times, as the performance test does, and we're looking at 0.005s for direct access vs 0.056s for proxy access.