1
0
Fork 0
easejs/doc/impl-details.texi

1656 lines
66 KiB
Plaintext

@c This document is part of the GNU ease.js manual.
@c Copyright (C) 2011, 2012, 2013, 2014 Free Software Foundation, Inc.
@c Permission is granted to copy, distribute and/or modify this document
@c under the terms of the GNU Free Documentation License, Version 1.3 or
@c any later version published by the Free Software Foundation; with no
@c Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
@c A copy of the license is included in the section entitled ``GNU Free
@c Documentation License''.
@node Implementation Details
@appendix Implementation Details / Rationale
The majority of the development time spent on ease.js was not hacking away
at the source code. Rather, it was spent with pen and paper. Every aspect of
ease.js was heavily planned from the start. Every detail was important to
ensure a consistent implementation that worked, was fast and that developers
would enjoy working with. Failures upfront or alterations to the design in
later versions would break backwards compatibility unnecessarily and damage
the reputation of the project.
When using ease.js, developers may wonder why things were implemented in the
manner that they were. Perhaps they have a problem with the implementation,
or just want to learn how the project works. This project was an excellent
learning experience that deals very closely with the power and flexibility
of prototypal programming. In an attempt to appease both parties, this
appendix is provided to provide some details and rationale behind ease.js.
@menu
* Class Module Design::
* Visibility Implementation::
* Internal Methods/Objects::
@end menu
@node Class Module Design
@section Class Module Design
The @var{Class} module, which is accessible via @samp{require(
'easejs').Class}, is the backbone of the entire project. In a class-based
Object-Oriented model, as one could guess by the name, the class is the star
player. When the project began, this was the only initial implementation
detail. Everything else was later layered atop of it.
As such, developing the Class module took the most thought and presented the
largest challenge throughout the project. Every detail of its implementation
exists for a reason. Nothing was put in place because the author simply
``felt like it''. The project aims to exist as a strong, reliable standard
for the development of JavaScript-based applications. If such a goal is to
be attained, the feature set and implementation details would have to be
strongly functional, easy to use and make sense to the Object-Oriented
developer community.
The design also requires a strong understanding of Object-Oriented
development. Attention was paid to the nuances that could otherwise
introduce bugs or an inconsistent implementation.
@menu
* Class Declaration Syntax::
* Class Storage::
* Constructor Implementation::
* Static Implementation::
@end menu
@node Class Declaration Syntax
@subsection Class Declaration Syntax
Much thought was put into how a class should be declared. The chosen style
serves as syntatic sugar, making the declarations appear very similar to
classes in other Object-Oriented languages.
The original style was based on John Resig's blog post about a basic means
of extending class-like objects (@pxref{About}). That style was
@samp{Class.extend()} to declare a new class and @samp{Foo.extend()} to
extend an existing class. This implementation is still supported for
creating anonymous classes. However, a means needed to be provided to create
named classes. In addition, invoking @code{extend()} on an empty class
seemed unnecessary.
The next incarnation made the @var{Class} module invokable. Anonymous
classes could be defined using @samp{Class( @{@} )} and named classes could
be defined by passing in a string as the first argument: @samp{Class( 'Foo',
@{@} )}. Classes could still be extended using the previously mentioned
syntax, but that did no justice if we need to provide a class name.
Therefore, the @samp{Class( 'SubFoo' ).extend( Supertype, @{@} )} syntax was
also adopted.
JavaScript's use of curly braces to represent objects provides a very
convenient means of making class definitions look like actual class
definitions. By convention, the opening brace for the declaration object is
on its own line, to make it look like an opening block.
@float Figure, f:class-def-syntax
@verbatim
Class( 'Foo' )
.implement( Bar )
.extend(
{
'public foo': function()
{
}
} );
@end verbatim
@caption{Syntax and style of class definition}
@end float
Syntax for implementing interfaces and extending classes was another
consideration. The implementation shown above was chosen for a couple of
reasons. Firstly, verbs were chosen in order to (a) prevent the use of
reserved words and (b) to represent that the process was taking place at
@emph{runtime}, @emph{as} the code was being executed. Unlike a language
like C++ or Java, the classes are not prepared at compile-time.
@node Class Storage
@subsection Class Storage
One of the more powerful features of ease.js is how classes (and other
objects, such as Interfaces) are stored. Rather than adopting its own model,
the decision was instead to blend into how JavaScript already structures its
data. Everything in JavaScript can be assigned to a variable, including
functions. Classes are no different.
One decision was whether or not to store classes internally by name, then
permit accessing it globally (wherever ease.js is available). This is how
most Object-Oriented languages work. If the file in which the class is
defined is available, the class can generally be referenced by name. This
may seem natural to developers coming from other Object-Oriented languages.
The decision was to @emph{not} adopt this model.
By storing classes @emph{only} in variables, we have fine control over the
scope and permit the developer to adopt their own mechanism for organizing
their classes. For example, if the developer wishes to use namespacing, then
he/she is free to assign the class to a namespace (e.g.
@samp{org.foo.my.ns.Foo = Class( @{@} )}). More importantly, we can take
advantage of the CommonJS format that ease.js was initially built for by
assigning the class to @code{module.exports}. This permits @samp{require(
'filename' )} to return the class.
This method also permits defining anonymous classes (while not necessarily
recommended, they have their uses just as anonymous functions do), mimic the
concept of Java's inner classes and create temporary classes
(@pxref{Temporary Classes}). Indeed, we can do whatever scoping that
JavaScript permits.
@subsubsection Memory Management
Memory management is perhaps one of the most important considerations.
Initially, ease.js encapsulated class metadata and visibility structures
(@pxref{Hacking Around the Issue of Encapsulation}). However, it quickly
became apparent that this method of storing data, although excellent for
protecting it from being manipulated, caused what appeared to be memory
leaks in long-running software. These were in fact not memory leaks, but
ease.js keeping references to class data with no idea when to free them.
To solve this issue, all class data is stored within the class itself (that
is, the constructor in JavaScript terms). They are stored in obscure
variables that are non-enumerable and subject to change in future releases.
This ensures that developers cannot rely on using them for reflection
purposes or for manipulating class data during runtime. This is important,
since looking at such members can give access to protected and private
instance data. In the future, the names may be randomly chosen at runtime to
further mitigate exploits. Until that time, developers should be aware of
potential security issues.
If the globally accessible model would have been adopted (storing classes
internally by class name rather than in variables), classes would not have
been freed from memory when they went out of scope. This raises the memory
footprint unnecessarily, especially for temporary classes. It would make
sense that, after a temporary class is done being used, that the class be
freed from memory.
Given this fact alone, the author firmly believes that the model that was
chosen was the best choice.
@node Constructor Implementation
@subsection Constructor Implementation
ease.js uses a PHP-style constructor. Rather than using the class name as
the constructor, a @code{__construct()} method is used. This was chosen
primarily because ease.js does not always know the name of the class. In
fact, in the early stages of development, named classes were unsupported.
With the PHP-style constructor, the class name does not need to be known,
allowing constructors to be written for anonymous and named classes alike.
In addition, the PHP-style constructor is consistent between class
definitions. To look up a constructor, one need only search for
``__construct'', rather than the class name. This makes certain operations,
such as global searching (using @command{grep} or any other utility), much
simpler.
One difference from PHP is the means of preventing instantiation. In PHP, if
the constructor is declared as non-public, then an error will be raised when
the developer attempts to instantiate the class. ease.js did not go this
route, as the method seems cryptic. Instead, an exception should be thrown
in the constructor if the developer doesn't wish the class to be
instantiated. In the future, a common method may be added for
consistency/convenience.
The constructor is optional. If one is not provided, nothing is done after
the class is instantiated (aside from the internal ease.js initialization
tasks).
The constructor is called after all initialization tasks have been
completed.
@node Static Implementation
@subsection 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.
@subsubsection 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 @emph{by
reference}. What does this mean? Consider two classes: @var{Foo} and
@var{SubFoo}, the latter of which inherits from the former. @var{Foo}
defines a static property @var{count} to be incremented each time the class
is instantiated. The subtype @var{SubFoo}, when instantiated (assuming the
constructor is not overridden), would increment that very same count.
Therefore, we can represent this by stating that @samp{Foo.count ===
SubFoo.count}. In the example below, we demonstrate this concept in
pseudocode:
@float Figure, f:static-ref-pseudocode
@verbatim
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
@end verbatim
@caption{Representing static properties in pseudocode}
@end float
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.
@subsubsection Emulating References
Fortunately, ECMAScript 5 provides a means to @emph{emulate} references --
getters and setters. Taking a look at @ref{f:static-ref-pseudocode}, we can
clearly see that @var{Foo} and @var{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 @var{SubFoo} and @emph{forwarding} gets/sets to the
supertype:
@float Figure, f:static-ref-forward
@verbatim
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
@end verbatim
@caption{Emulating references with getters/setters (proxy)}
@end float
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. @emph{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.
@subsubsection 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
@samp{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:
@enumerate
@item
Add another object to be shared between classes (e.g. @samp{Foo.$}).
@item
Do not inherit by reference. Each subtype would have their own distinct
value.
@item
Access properties via an accessor method (e.g. @samp{Foo.$('var')}),
allowing us to properly proxy much like a getter/setter.
@end enumerate
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 @emph{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 @emph{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:
@float Figure, f:static-accessor-impl
@verbatim
// external
Foo.$('var'); // getter
Foo.$( 'var, 'foo' ); // setter
// internal
this.__self.$('var'); // getter
this.__self.$( 'var', 'foo' ); // setter
@end verbatim
@caption{Accessor implementation for static properties}
@end float
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:
@itemize
@item
It provides an implementation that is @emph{consistent with other
Object-Oriented languages}. This is the most important point.
@item
The accessor method parameter style is common in other frameworks like
jQuery.
@item
The method name (@var{$}) is commonly used to denote a variable in scripting
languages (such as PHP and shells, or to denote a scalar in Perl).
@item
It works consistently in ES5 and pre-ES5 environments alike.
@end itemize
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.
@subsubsection 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 @emph{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:
@itemize
@item
This may make libraries written using ease.js unportable (to older
environments). If written using an ES5-only syntax, they would have no way
to fall back for static properties.
@item
The syntax differences could be very confusing, especially to those
beginning to learn ease.js. They may not clearly understand the differences,
or may go to use a library in their own code, and find that things do not
work as intended. Code examples would also have to make clear note of what
static syntax they decided to use. It adds a layer of complexity.
@end itemize
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.
@node Visibility Implementation
@section Visibility Implementation
One of the major distinguishing factors of ease.js is its full visibility
support (@pxref{Access Modifiers}). This feature was the main motivator
behind the project. Before we can understand the use of this feature, we
have to understand certain limitations of JavaScript and how we may be able
to work around them.
@menu
* Encapsulation In JavaScript::
* Hacking Around the Issue of Encapsulation::
* The Visibility Object::
* Method Wrapping::
* Pre-ES5 Fallback::
@end menu
@node Encapsulation In JavaScript
@subsection Encapsulation In JavaScript
Encapsulation is a cornerstone of many strong software development paradigms
(@pxref{Encapsulation}). This concept is relatively simply to achieve using
closures in JavaScript, as shown in the following example stack
implementation:
@float Figure, f:js-encapsulation-ex
@verbatim
var stack = {};
( function( exports )
{
var data = [];
exports.push = function( data )
{
data.push( data );
};
exports.pop = function()
{
return data.pop();
};
} )( stack );
stack.push( 'foo' );
stack.pop(); // foo
@end verbatim
@caption{Encapsulation example using closures in JavaScript}
@end float
Because functions introduce scope in JavaScript, data can be hidden within
them. In @ref{f:js-encapsulation-ex} above, a self-executing function is
used to encapsulate the actual data in the stack (@var{data}). The function
accepts a single argument, which will hold the functions used to push and
pop values to/from the stack respectively. These functions are closures that
have access to the @var{data} variable, allowing them to alter its data.
However, nothing outside of the self-executing function has access to the
data. Therefore, we present the user with an API that allows them to
push/pop from the stack, but never allows them to see what data is actually
@emph{in} the stack@footnote{The pattern used in the stack implementation is
commonly referred to as the @dfn{module} pattern and is the same concept
used by CommonJS. Another common implementation is to return an object
containing the functions from the self-executing function, rather than
accepting an object to store the values in. We used the former
implementation here for the sake of clarity and because it more closely
represents the syntax used by CommonJS.}.
Let's translate some of the above into Object-Oriented terms:
@itemize
@item @var{push} and @var{pop} are public members of @var{stack}.
@item @var{data} is a private member of @var{stack}.
@item @var{stack} is a Singleton.
@end itemize
We can take this a bit further by defining a @code{Stack} prototype so that
we can create multiple instances of our stack implementation. A single
instance hardly seems useful for reuse. However, in attempting to do so, we
run into a bit of a problem:
@float Figure, f:js-proto-inst-noencapsulate
@verbatim
var Stack = function()
{
this._data = [];
};
Stack.prototype = {
push: function( val )
{
this._data.push( val );
},
pop: function()
{
return this._data.pop();
},
};
// create a new instance of our Stack object
var inst = new Stack();
// what's this?
inst.push( 'foo' );
console.log( inst._data ); // [ 'foo' ]
// uh oh.
inst.pop(); // foo
console.log( inst._data ); // []
@end verbatim
@caption{Working easily with instance members in JavaScript breaks
encapsulation}
@end float
By defining our methods on the prototype and our data in the constructor, we
have created a bit of a problem. Although the data is easy to work with,
@emph{it is no longer encapsulated}. The @var{_data} property is now public,
accessible for the entire work to inspect and modify. As such, a common
practice in JavaScript is to simply declare members that are "supposed to
be" private with an underscore prefix, as we have done above, and then trust
that nobody will make use of them. Not a great solution.
Another solution is to use a concept called @dfn{privileged members}, which
uses closures defined in the constructor rather than functions defined in
the prototype:
@float Figure, f:js-privileged-members
@verbatim
var Stack = function()
{
var data = [];
this.push = function( data )
{
data.push( data );
};
this.pop = function()
{
return data.pop();
};
};
// create a new instance of our Stack object
var inst = new Stack();
// can no longer access "privileged" member _data
inst.push( 'foo' );
console.log( inst._data ); // undefined
@end verbatim
@caption{Privileged members in JavaScript}
@end float
You may notice a strong similarity between @ref{f:js-encapsulation-ex} and
@ref{f:js-privileged-members}. They are doing essentially the same thing,
the only difference being that @ref{f:js-encapsulation-ex} is returning a
single object and @ref{f:js-privileged-members} represents a constructor
that may be instantiated.
When using privileged members, one would define all members that need access
to such members in the constructor and define all remaining members in the
prototype. However, this introduces a rather large problem that makes this
design decision a poor one in practice: @emph{Each time @var{Stack} is
instantiated, @var{push} and @var{pop} have to be redefined, taking up
additional memory and CPU cycles}. Those methods will be kept in memory
until the instance of @var{Stack} is garbage collected.
In @ref{f:js-privileged-members}, these considerations may not seem like
much of an issue. However, consider a constructor that defines tens of
methods and could potentially have hundreds of instances. For this reason,
you will often see the concepts demonstrated in
@ref{f:js-proto-inst-noencapsulate} used more frequently in libraries that
have even modest performance requirements.
@node Hacking Around the Issue of Encapsulation
@subsection Hacking Around the Issue of Encapsulation
Since neither @ref{f:js-encapsulation-ex} nor @ref{f:js-privileged-members}
are acceptable implementations for strong Classical Object-Oriented code,
another solution is needed. Based on what we have seen thus far, let's
consider our requirements:
@itemize
@item
Our implementation must not break encapsulation. That is---we should be
enforcing encapsulation, not simply trusting our users not to touch.
@item
We must be gentle with our memory allocations and processing. This means
placing @emph{all} methods within the prototype.
@item
We should not require any changes to how the developer uses the
constructor/object. It should operate just like any other construct in
JavaScript.
@end itemize
We can accomplish the above by using the encapsulation concepts from
@ref{f:js-encapsulation-ex} and the same prototype model demonstrated in
@ref{f:js-proto-inst-noencapsulate}. The problem with
@ref{f:js-encapsulation-ex}, which provided proper encapsulation, was that
it acted as a Singleton. We could not create multiple instances of it and,
even if we could, they would end up sharing the same data. To solve this
problem, we need a means of distinguishing between each of the instances so
that we can access the data of each separately:
@float Figure, f:js-encapsulate-instance
@verbatim
var Stack = ( function()
{
var idata = [],
iid = 0;
var S = function()
{
// set the instance id of this instance, then increment it to ensure
// the value is unique for the next instance
this.__iid = iid++;
// initialize our data for this instance
idata[ this.__iid ] = {
stack: [],
};
}:
S.prototype = {
push: function( val )
{
idata[ this.__iid ].stack.push( val );
},
pop: function()
{
return idata[ this.__iid ].stack.pop();
}
};
return S;
} )();
var stack1 = new Stack();
var stack2 = new Stack();
stack1.push( 'foo' );
stack2.push( 'bar' );
stack1.pop(); // foo
stack2.pop(); // bar
@end verbatim
@caption{Encapsulating data per instance}
@end float
This would seem to accomplish each of our above goals. Our implementation
does not break encapsulation, as nobody can get at the data. Our methods are
part of the @var{Stack} prototype, so we are not redefining it with each
instance, eliminating our memory and processing issues. Finally, @var{Stack}
instances can be instantiated and used just like any other object in
JavaScript; the developer needn't adhere to any obscure standards in order
to emulate encapsulation.
Excellent! However, our implementation does introduce a number of issues
that we hadn't previously considered:
@itemize
@item
Our implementation is hardly concise. Working with our ``private''
properties requires that we add ugly instance lookup code@footnote{We could
encapsulate this lookup code, but we would then have the overhead of an
additional method call with very little benefit; we cannot do something
like: @samp{this.stack}.}, obscuring the actual domain logic.
@item
Most importantly: @emph{this implementation introduces memory leaks}.
@end itemize
What do we mean by ``memory leaks''? Consider the usage example in
@ref{f:js-encapsulate-instance}. What happens when were are done using
@var{stack1} and @var{stack2} and they fall out of scope? They will be GC'd.
However, take a look at our @var{idata} variable. The garbage collector will
not know to free up the data for our particular instance. Indeed, it cannot,
because we are still holding a reference to that data as a member of the
@var{idata} array.
Now imagine that we have a long-running piece of software that makes heavy
use of @var{Stack}. This software will use thousands of instances throughout
its life, but they are used only briefly and then discarded. Let us also
imagine that the stacks are very large, perhaps holding hundreds of
elements, and that we do not necessarily @code{pop()} every element off of
the stack before we discard it.
Imagine that we examine the memory usage throughout the life of this
software. Each time a stack is used, additional memory will be allocated.
Each time we @code{push()} an element onto the stack, additional memory is
allocated for that element. Because our @var{idata} structure is not freed
when the @var{Stack} instance goes out of scope, we will see the memory
continue to rise. The memory would not drop until @var{Stack} itself falls
out of scope, which may not be until the user navigates away from the page.
From our perspective, this is not a memory leak. Our implementation is
working exactly as it was developer. However, to the user of our stack
implementation, this memory management is out of their control. From their
perspective, this is indeed a memory leak that could have terrible
consequences on their software.
This method of storing instance data was ease.js's initial
``proof-of-concept'' implementation (@pxref{Class Storage}). Clearly, this
was not going to work; some changes to this implementation were needed.
@anchor{Instance Memory Considerations}
@subsubsection Instance Memory Considerations
JavaScript does not provide destructors to let us know when an instance is
about to be GC'd, so we unfortunately cannot know when to free instance data
from memory in @ref{f:js-encapsulate-instance}. We are also not provided
with an API that can return the reference count for a given object. We could
provide a method that the user could call when they were done with the
object, but that is not natural to a JavaScript developer and they could
easily forget to call the method.
As such, it seems that the only solution for this rather large issue is to
store instance data on the instance itself so that it will be freed with the
instance when it is garbage collected (remember, we decided that privileged
members were not an option in the discussion of
@ref{f:js-privileged-members}). Hold on - we already did that in
@ref{f:js-proto-inst-noencapsulate}; that caused our data to be available
publicly. How do we approach this situation?
If we are adding data to an instance itself, there is no way to prevent it
from being accessed in some manner, making true encapsulation impossible.
The only options are to obscure it as best as possible, to make it too
difficult to access in any sane implementation. For example:
@itemize
@item
The property storing the private data could be made non-enumerable,
requiring the use of a debugger or looking at the source code to determine
the object name.
@itemize
@item This would work only with ECMAScript 5 and later environments.
@end itemize
@item
We could store all private data in an obscure property name, such as
@var{___$$priv$$___}, which would make it clear that it should not be
accessed.
@itemize
@item
We could take that a step further and randomize the name, making it very
difficult to discover at runtime, especially if it were
non-enumerable@footnote{Note that ease.js does not currently randomize its
visibility object name.}.
@end itemize
@end itemize
Regardless, it is clear that our data will only be ``encapsulated'' in the
sense that it will not be available conveniently via a public API. Let's
take a look at how something like that may work:
@float Figure, f:js-obscure-private
@verbatim
var Stack = ( function()
{
// implementation of getSomeRandomName() is left up to the reader
var _privname = getSomeRandomName();
var S = function()
{
// define a non-enumerable property to store our private data (will
// only work in ES5+ environments)
Object.defineProperty( this, _privname, {
enumerable: false,
writable: false,
configurable: false,
value: {
stack: []
}
} );
};
S.prototype = {
push: function( val )
{
this[ _privname ].stack.push( val );
},
pop: function()
{
return this[ _privname ].stack.pop();
},
};
return S;
} );
var inst = new Stack();
inst.push( 'foo' );
inst.pop(); // foo
@end verbatim
@caption{Using a random, non-enumerable property name to store private
members}
@end float
Now we are really starting to hack around what JavaScript provides for us.
We seem to be combining the encapsulation issues presented in
@ref{f:js-proto-inst-noencapsulate} and the obscurity demonstrated in
@ref{f:js-encapsulate-instance}. In addition, we our implementation depends
on ECMAScript 5 (ideally, we would detect that and fall back to normal,
enumerable properties in pre-ES5 environments, which ease.js does indeed
do). This seems to be a case of encapsulation through obscurity@footnote{A
play on ``security through obscurity''.}. While our implementation certainly
makes it difficult to get at the private member data, it is also very
obscure and inconvenient to work with. Who wants to write Object-Oriented
code like that?
@subsubsection Other Considerations
We have conveniently omitted a number of other important factors in our
discussion thus far. Before continuing, they deserve some mention and
careful consideration.
How would we implement private methods? We could add them to our private
member object, just as we defined @var{stack} in @ref{f:js-obscure-private},
but that would cause it to be redefined with each instance, raising the same
issues that were discussed with @ref{f:js-privileged-members}. Therefore, we
would have to define them in a separate ``prototype'', if you will, that
only we have access to:
@float Figure, f:js-obscure-private-methods
@verbatim
var Stack = ( function()
{
// implementation of getSomeRandomName() is left up to the reader
var _privname = getSomeRandomName();
var S = function()
{
// define a non-enumerable property to store our private data (will
// only work in ES5+ environments)
Object.defineProperty( this, _privname, {
// ... (see previous example)
} );
};
// private methods that only we will have access to
var priv_methods = {
getStack: function()
{
// note that, in order for 'this' to be bound to our instance,
// it must be passed as first argument to call() or apply()
return this[ _privname ].stack;
},
};
// public methods
S.prototype = {
push: function( val )
{
var stack = priv_methods.getStack.call( this );
stack.push( val );
},
pop: function()
{
var stack = priv_methods.getStack.call( this );
return stack.pop();
},
};
return S;
} );
var inst = new Stack();
inst.push( 'foo' );
inst.pop(); // foo
@end verbatim
@caption{A possible private method implementation}
@end float
While this does solve our problem, it further reduces code clarity. The
implementation in @ref{f:js-obscure-private-methods} is certainly a far cry
from something like @samp{this._getStack()}, which is all you would need to
do in ease.js.
Another consideration is a protected (@pxref{Access Modifiers}) member
implementation, the idea being that subtypes should inherit both public and
protected members. Inheritance is not something that we had to worry about
with private members, so this adds an entirely new layer of complexity to
the implementation. This would mean somehow making a protected prototype
available to subtypes through the public prototype. Given our implementation
in the previous figures, this would likely mean an awkward call that
somewhat resembles: @samp{this[ _protname ].name}.
Although the implementations show in @ref{f:js-obscure-private} and
@ref{f:js-obscure-private-methods} represent a creative hack, this is
precisely one of the reasons ease.js was created - to encapsulate such
atrocities that would make code that is difficult to use, hard to maintain
and easy to introduce bugs. One shouldn't have to have a deep understanding
of JavaScript's prototype model in order to write the most elementary of
Classical Object-Oriented code. For example, the constructors in the
aforementioned figures directly set up an object in which to store private
members. ease.js will do this for you before calling the
@code{__construct()} method. Furthermore, ease.js does not require
referencing that object directly, like we must do in our methods in
@ref{f:js-obscure-private}. Nor does ease.js have an awkward syntax for
invoking private methods. We will explore how this is handled in the
following section.
@node The Visibility Object
@subsection The Visibility Object
Let's consider how we may rewrite @var{Stack} in
@ref{f:js-obscure-private-methods} using ease.js:
@float Figure, f:stack-using-easejs
@verbatim
var Stack = Class( 'Stack',
{
'private _data': [],
'public push': function( val )
{
this._data.push( val );
},
'public pop': function()
{
return this._data.pop();
}
} );
var inst = Stack();
inst.push( 'foo' );
inst.pop(); // foo
@end verbatim
@caption{Stack implementation using ease.js}
@end float
The above implementation is much less impressive looking than our prior
examples. What we have done is encapsulate the excess logic needed to
emulate a class and got right down to business. ease.js will take the class
definition above and generate an object much like we had done in the prior
examples, with a few improvements.
If you have not read over the previous sections, you are recommended to do
so before continuing in order to better understand the rationale and finer
implementation details.
The secret behind ease.js's visibility implementation (@pxref{Access
Modifiers}) is referred to internally as the @dfn{visibility object} (or, in
older commits and some notes, the @dfn{property object}). Consider the
problem regarding the verbosity of our private property accessors and method
calls in @ref{f:js-obscure-private-methods}. It would be much more
convenient if the properties and methods were bound to @var{this} so that
they can be accessed more naturally, as would be expected by a programmer
familiar with classes in other Classical Object-Oriented languages
(@pxref{f:stack-using-easejs}). This can be done using @code{call()} or
@code{apply()}:
@float Figure, f:function-context
@verbatim
function getName()
{
return this.name;
}
var obj = { name: "foo" };
getName.call( obj ); // "foo"
@end verbatim
@caption{Calling a function within the context of a given object}
@end float
@ref{f:function-context} demonstrates the concept we are referring to. Given
an arbitrary object @var{obj}, we can call any given method (in this case,
@code{getName()}, binding @var{this} to that object. This is precisely what
ease.js does with each method call. To understand this process, we have to
explore two concepts: the visibility object itself and method wrapping. We
will start by discussing the visibility object in more detail and cover
method wrapping later on (@pxref{Method Wrapping}).
@menu
* Visibility Object Implementation:: Design of the visibility object
* Property Proxies:: Overcoming prototype limitations
@end menu
@node Visibility Object Implementation
@subsubsection Visibility Object Implementation
The visibility object is mostly simply represented in the following diagram:
@float Figure, f:visobj
@image{img/visobj}
@caption{Structure of the visibility object}
@end float
Specifically, the visibility object is a prototype chain containing the
private members of the class associated with the method currently being
invoked on the current instance, its protected members (including those
inherited from its supertype) and the public members (also including those
inherited from the supertype). To accomplish this, the visibility object has
the following properties:
@itemize
@item
The private object is @dfn{swappable} - that is, it is the only portion of
the prototype chain that is replaced between calls to various methods.
@itemize
@item
It is for this reason that the private object is placed atop the prototype
chain. This allows it to be swapped very cheaply by simply passing
different objects to be bound to @code{this}.
@end itemize
@item
Both the private and protected objects are initialized during instantiation
by the @code{__initProps()} method attached by @code{ClassBuilder} to each
class during definition.
@itemize
@item
Properties are cloned to ensure references are not shared between
instances.
@item
Methods are copied by reference, since their implementations are
immutable.
@item
This must be done because neither protected nor private members may be
added to the prototype chain of a class.
@itemize
@item
Doing so would effectively make them public.
@item
Doing so would also cause private members to be inherited by subtypes.
@end itemize
@end itemize
@item
Public members are a part of the class prototype chain as you would expect
in any conventional prototype.
@itemize
@item
Public @emph{properties} only are initialized by @code{__initProps()},
just as private and protected properties, to ensure that no references are
shared between instances.
@end itemize
@end itemize
As a consequence of the above, it is then clear that there must be a
separate visibility object (prototype chain) @emph{for each supertype of
each instance}, because there must be a separate private object for each
subtype of each instance. Let us consider for a moment why this is necessary
with the following sample of code:
@float Figure, f:priv-swap-rationale
@verbatim
var C1 = Class(
{
'private _name': 'Foo',
'public getName': function()
{
return this._name;
},
// ...
} ),
// note the naming convention using descending ids for the discussion
// following this example
C0 = C1.extend(
{
// ...
} );
C1().getName(); // "Foo"
C0().getName(); // "Foo"
@end verbatim
@caption{Why private member swapping is necessary}
@end float
@ref{f:priv-swap-rationale} demonstrates why the private object
swapping@footnote{The term ``swapping'' can be a bit deceptive. While we are
swapping in the sense that we are passing an entirely new private object as
the context to a method, we are @emph{not} removing an object from the
prototype chain and adding another in place of it. They @emph{do}, however,
share the same prototype chain.} is indeed necessary. If a subtype does
@emph{not} override a super method that uses a private member, it is
important that the private member be accessible to the method when it is
called. In @ref{f:priv-swap-rationale}, if we did not swap out the object,
@var{_name} would be undefined when invoked on @var{C2}.
Given this new information, the implementation would more formally be
represented as a collection of objects @var{V} for class @var{C} and each of
its supertypes as denoted by @var{C\_n}, with @var{C\_0} representing the
class having been instantiated and any integer @var{n} > 0 representing the
closest supertype, such that each @var{V\_n} is associated with @var{C\_n},
@var{V\_n\^x} is the visibility object bound to any method associated with
class @var{C\_x} and each @var{V} shares the same prototype chain @var{P\_n}
for any given instance of @var{C\_n}:
@float Figure, f:visobj-collection
@image{img/visobj-collection-wide}
@caption{Collection of visibility objects @var{V} for each class @var{C}}
@end float
Fortunately, as shown in @ref{f:visobj-collection}, the majority of the
prototype chain can be reused under various circumstances:
@itemize
@item
For each instance of class @var{C\_n}, @var{P\_n} is re-used as the
prototype of every @var{V\_n}.
@item
@var{C\_n} is re-used as the prototype for each @var{P\_n}.
@end itemize
Consequently, on instantiation of class @var{C\_n}, we incur a performance
hit from @code{__initProps()} for the initialization of each member of
@var{V\_x} and @var{P\_x}, as well as each property of @var{C\_x},
recursively for each value of @var{m} >= @var{x} >= @var{n} (that is,
supertypes are initialized first), where @var{m} is equal to the number of
supertypes of class @var{C\_n} + 1.@footnote{There is room for optimization
in this implementation, which will be left for future versions of ease.js.}
The instance stores a reference to each of the visibility objects @var{V},
indexed by an internal class identifier (which is simply incremented for
each new class definition, much like we did with the instance id in
@ref{f:js-encapsulate-instance}). When a method is called, the visibility
object that matches the class identifier associated with the invoked method
is then passed as the context (bound to @var{this}) for that method
(@pxref{Method Wrapping}).
@node Property Proxies
@subsubsection Property Proxies
Astute readers may notice that the visibility implementation described in
the previous section (@pxref{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 @emph{not necessarily on its correct object}.
The following example will demonstrate this issue:
@float Figure, f:proto-set-issue
@verbatim
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"
@end verbatim
@caption{Demonstrating property set issues with prototype chains}
@end float
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 @var{vis} in @ref{f:proto-set-issue} has
no value for property @var{foo} (note that a value of @code{undefined} is
still a value), it looks at its prototype, @var{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 @var{foo} of @var{vis}, we are in fact setting the
property on @var{vis} itself, @emph{not} @var{pub}. This fills that
aforementioned ``hole'', masking the value further down the prototype chain
(our value in @var{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 @emph{only that visibility
object}.
To summarize:
@itemize
@item
Methods are never an issue, as they are immutable (in the sense of a class).
@item
Reading properties are never an issue; they properly ``peek'' through holes
in the prototype chain.
@item
Writing private values are never an issue, as they will be properly set on
that visibility object. The value needn't be set on any other visibility
objects, since private values are to remain exclusive to that instance
within the context of that class only (it should not be available to methods
of supertypes).
@item
We run into issues when @emph{setting} public or protected values, as they
are not set on their appropriate object.
@end itemize
This issue is huge. Before ECMAScript 5, it may have been a show-stopper,
preventing us from using a familiar @code{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 @ref{f:proto-set-issue}:
@float Figure, f:proto-getset
@verbatim
// 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"
@end verbatim
@caption{Using getters/setters to proxy values to the appropriate object}
@end float
The implementation in @ref{f:proto-getset} is precisely how ease.js
implements and @emph{enforces} the various levels of
visibility.@footnote{One may wonder why we implemented a getter in
@ref{f:proto-getset} when we had no trouble retrieving the value to begin
with. In defining a @emph{setter} for @var{foo} on object @var{vis}, we
filled that ``hole'', preventing us from ``seeing through'' into the
prototype (@var{pub}). Unfortunately, that means that we must use a getter
in order to provide the illusion of the ``hole''.} 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:
@itemize
@item
For each public property, proxy from the protected object to the public.
@item
For each protected property, proxy from the private object to the
protected.@footnote{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, @emph{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.}
@end itemize
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 @emph{always} incur a
performance hit@footnote{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.}, 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.
@node Method Wrapping
@subsection Method Wrapping
The visibility object (@pxref{The Visibility Object}) is a useful tool for
organizing the various members, but we still need some means of binding it
to a method call. This is accomplished by wrapping each method in a closure
that, among other things@footnote{The closure also sets the @code{__super()}
method reference, if a super method exists, and returns the instance if
@var{this} is returned from the method.}, uses @code{apply()} to forward the
arguments to the method, binding @var{this} to the appropriate visibility
object. This is very similar to the ES5 @code{Function.bind()} call.
The following example demonstrates in an overly-simplistic way how ease.js
handles class definitions and method wrapping.@footnote{ease.js, of course,
generates its own visibility objects internally. However, for the sake of
brevity, we simply provide one in our example.}
@float Figure, f:method-wrapping
@verbatim
/**
* Simple function that returns a prototype ("class"), generated from the
* given definition and all methods bound to the provided visibility object
*/
function createClass( vis, dfn )
{
var C = function() {},
hasOwn = Object.hasOwnProperty;
for ( name in dfn )
{
// ignore any members that are not part of our object (further down
// the chain)
if ( hasOwn.call( dfn, name ) === false )
{
continue;
}
// simply property impl (WARNING: copies by ref)
if ( typeof dfn[ name ] !== 'function' )
{
C.prototype[ name ] = dfn[ name ];
continue;
}
// enclose name in a closure to preserve it (otherwise it'll contain
// the name of the last member in the loop)
C.prototype[ name ] = ( function( mname )
{
return function()
{
// call method with the given argments, bound to the given
// visibility object
dfn[ mname ].apply( vis, arguments );
};
} )( name );
}
return C;
};
var vis = { _data: "foo" },
Foo = createClass( vis,
{
getData: function()
{
return this._data;
},
} );
var inst = new Foo();
// getData() will be bound to vis and should return its _data property
inst.getData(); // "foo"
@end verbatim
@caption{Basic "class" implementation with method binding}
@end float
There are some important considerations with the implementation in
@ref{f:method-wrapping}, as well as ease.js's implementation:
@itemize
@item
Each method call, unless optimized away by the engine, is equivalent to two
function invocations, which cuts down on the available stack space.
@itemize
@item
The method wrapping may complicate tail call optimization, depending on
the JavaScript engine's implementation and whether or not it will optimize
across the stack, rather than just a single-depth recursive call.
@item
As such, for operations that are highly dependent on stack space, one may
wish to avoid method calls and call functions directly.
@end itemize
@item
There is a very slight performance hit (though worrying about this is likely
to be a micro-optimization in the majority of circumstances).
@end itemize
As mentioned previously, each visibility object is indexed by class
identifier (@pxref{Visibility Object Implementation}). The appropriate
visibility object is bound dynamically on method invocation based on the
matching class identifier. Previously in this discussion, it was not clear
how this identifier was determined at runtime. Since methods are shared by
reference between subtypes, we cannot store a class identifier on the
function itself.
The closure that wraps the actual method references the arguments that were
passed to the function that created it when the class was defined. Among
these arguments are the class identifier and a lookup method used to
determine the appropriate visibility object to use for binding.@footnote{See
@file{lib/MethodWrappers.js} for the method wrappers and
@code{ClassBuilder.getMethodInstance()} for the lookup function.} Therefore,
the wrapper closure will always know the appropriate class identifier. The
lookup method is also passed @var{this}, which is bound to the instance
automatically by JavaScript for the method call. It is on this object that
the visibility objects are stored (non-enumerable; @pxref{Instance Memory
Considerations}), indexed by class identifier. The appropriate is simply
returned.
If no visibility object is found, @code{null} is returned by the lookup
function, which causes the wrapper function to default to @var{this} as
determined by JavaScript, which will be the instance that the method was
invoked on, or whatever was bound to the function via a call to
@code{call()} or @code{apply()}. This means that, currently, a visibility
object can be explicitly specified for any method by invoking the method in
the form of: @samp{inst.methodName.apply( visobj, arguments )}, which is
consistent with how JavaScript is commonly used with other prototypes.
However, it should be noted that this behavior is undocumented and subject
to change in future releases unless it is decided that this implementation
is ideal. It is therefore recommended to avoid using this functionality for
the time being.@footnote{One one hand, keeping this feature is excellent in
the sense that it is predictable. If all other prototypes work this way,
why not ``classes'' as created through ease.js? At the same time, this is
not very class-like. It permits manipulating the internal state of the
class, which is supposed to be encapsulated. It also allows bypassing
constructor logic and replacing methods at runtime. This is useful for
mocking, but a complete anti-pattern in terms of Classical Object-Oriented
development.}
@subsubsection Private Method Performance
A special exception to GNU ease.js' method wrapping implementation is made
for private methods. As mentioned above, there are a number of downsides to
method wrapping, including effectively halving the remaining stack space for
heavily recursive operations, overhead of closure invocation, and thwarting
of tail call optimization. This situation is rather awkward, because it
essentially tells users that ease.js should not be used for
performance-critical invocations or heavily recursive algorithms, which is
very inconvenient and unintuitive.
To eliminate this issue for the bulk of program logic, method wrapping does
not occur on private methods. To see why it is not necessary, consider the
purpose of the wrappers:
@enumerate
@item
All wrappers perform a context lookup, binding to the instance's private
visibility object of the class that defined that particular method.
@item
This context is restored upon returning from the call: if a method returns
@var{this}, it is instead converted back to the context in which the method
was invoked, which prevents the private member object from leaking out of a
public interface.
@item
In the event of an override, @var{this.__super} is set up (and torn down).
@end enumerate
There are other details (e.g. the method wrapper used for @ref{Method
Proxies,,method proxies}), but for the sake of this particular discussion,
those are the only ones that really matter. Now, there are a couple of
important details to consider about private members:
@itemize
@item
Private members are only ever accessible from within the context of the
private member object, which is always the context when executing a method.
@item
Private methods cannot be overridden, as they cannot be inherited.
@end itemize
Consequently:
@enumerate
@item
We do not need to perform a context lookup: we are already in the proper
context.
@item
We do not need to restore the context, as we never needed to change it to
begin with.
@item
@var{this.__self} is never applicable.
@end enumerate
This is all the more motivation to use private members, which enforces
encapsulation; keep in mind that, because use of private members is the
ideal in well-encapsulated and well-factored code, ease.js has been designed
to perform best under those circumstances.
@node Pre-ES5 Fallback
@subsection Pre-ES5 Fallback
For any system that is to remain functionally compatible across a number of
environments, one must develop around the one with the least set of
features. In the case of ease.js, this means designing around the fact that
it must maintain support for older, often unsupported,
environments.@footnote{ease.js was originally developed for use in software
that would have to maintain compatibility as far back as IE6, while still
operating on modern web browsers and within a server-side environment.} The
line is drawn between ECMAScript 5 and its predecessors.
As mentioned when describing the proxy implementation (@pxref{Property
Proxies}), ease.js's ability to create a framework that is unobtrusive and
fairly easy to work with is attributed to features introduced in ECMAScript
5, primarily getters and setters. Without them, we cannot proxy between the
different visibility layers (@pxref{Visibility Object Implementation}). As a
consequence, @emph{we cannot use visibility layers within a pre-ES5
environment}.
This brings about the subject of graceful feature degradation. How do we
fall back while still allowing ease.js to operate the same in both
environments?
@itemize
@item
Because getters/setters are unsupported, we cannot proxy (@pxref{Property
Proxies}) between visibility layers (@pxref{Visibility Object
Implementation}).
@itemize
@item
Visibility support is enforced for development, but it is not necessary in
a production environment (unless that environment makes heavy use of 3rd
party libraries that may abuse the absence of the feature).
@itemize
@item
Therefore, the feature can be safely dropped.
@item
It is important that the developer develops the software in an
ECMAScript 5+ environment to ensure that the visibility constraints
are properly enforced. The developer may then rest assured that their
code will work properly in pre-ES5 environments (so long as they are
not using ES5 features in their own code).
@end itemize
@end itemize
@end itemize
@subsubsection Visibility Fallback
Visibility fallback is handled fairly simply in ease.js polymorphically with
the @code{FallbackVisibilityObjectFactory} prototype (as opposed to
@code{VisibilityObjectFactory} which is used in ES5+ environments), which
does the following:
@itemize
@item
Property proxies are unsupported. As such, rather than returning a proxy
object, @code{createPropProxy()} will simply return the object that was
originally passed to it.
@item
This will ultimately result in each layer (public, protected and private)
referencing the same object (the class prototype, also known as the
``public'' layer).
@itemize
@item
Consequently, all members will be public, just as they would have been
without visibility constraints.
@end itemize
@end itemize
Classical Object-Oriented programming has many rich features, but many of
its ``features'' are simply restrictions it places on developers. This
simple fact works to our benefit. However, in this case of a visibility
implementation, we aren't dealing only with restrictions. There is one
exception.
Unfortunately, this necessary fallback introduces a startling limitation:
Consider what might happen if a subtype defines a private member with the
same name as the supertype. Generally, this is not an issue. Subtypes have
no knowledge of supertypes' private members, so there is no potential for
conflict. Indeed, this is the case with our visibility implementation
(@pxref{Visibility Object Implementation}. Unfortunately, if we merge all
those layers into one, we introduce a potential for conflict.
@anchor{Private Member Dilemma}
@subsubsection Private Member Dilemma
With public and protected members (@pxref{Access Modifiers}), we don't have
to worry about conflicts because they are inherited by subtypes
(@pxref{Inheritance}). Private members are intended to remain distinct from
any supertypes; only that specific class has access to its own private
members. As such, inheritance cannot be permitted. However, by placing all
values in the prototype chain (the public layer), we are permitting
inheritance of every member. Under this circumstance, if a subtype were to
define a member of the same name as a supertype, it would effectively be
altering the value of its supertype. Furthermore, the supertype would have
access to the same member, allowing it to modify the values of its
@emph{subtypes}, which does not make sense at all!
This means that we have to place a certain restriction on ease.js as a
whole; we must prevent private member name conflicts even though they cannot
occur in ES5 environments. This is unfortunate, but necessary in order to
ensure feature compatibility across the board. This also has the consequence
of allowing the system to fall back purely for performance benefits (no
overhead of the visibility object).
@subsubsection Forefitting Fallbacks
Although ease.js allows flexibility in what environment one develops for, a
developer may choose to support only ES5+ environments and make use of ES5
features. At this point, the developer may grow frustrated with ease.js
limiting its implementation for pre-ES5 environments when their code will
not even run in a pre-ES5 environment.
For this reason, ease.js may include a feature in the future to disable
these limitations on a class-by-class@footnote{Will also include traits in
the future.} basis in order to provide additional syntax benefits, such as
omission of the static access modifiers (@pxref{Static Implementation}) and
removal of the private member conflict check.
@node Internal Methods/Objects
@section Internal Methods/Objects
There are a number of internal methods/objects that may be useful to
developers who are looking to use some features of ease.js without using the
full class system. An API will be provided to many of these in the future,
once refactoring is complete. Until that time, it is not recommended that
you rely on any of the functionality that is not provided via the public API
(@code{index.js} or the global @var{easejs} object).