[#5] Added method wrapping details for visibility implementation
parent
5ef46d4992
commit
412f8ad769
|
@ -422,6 +422,7 @@ around them.
|
||||||
* Encapsulation In JavaScript::
|
* Encapsulation In JavaScript::
|
||||||
* Hacking Around the Issue of Encapsulation::
|
* Hacking Around the Issue of Encapsulation::
|
||||||
* The Visibility Object::
|
* The Visibility Object::
|
||||||
|
* Method Wrapping::
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
@node Encapsulation In JavaScript
|
@node Encapsulation In JavaScript
|
||||||
|
@ -705,6 +706,7 @@ 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;
|
implementation (@pxref{Class Storage}). Clearly, this was not going to work;
|
||||||
some changes to this implementation were needed.
|
some changes to this implementation were needed.
|
||||||
|
|
||||||
|
@anchor{Instance Memory Considerations}
|
||||||
@subsubsection Instance Memory Considerations
|
@subsubsection Instance Memory Considerations
|
||||||
JavaScript does not provide destructors to let us know when an instance is about
|
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
|
to be GC'd, so we unfortunately cannot know when to free instance data from
|
||||||
|
@ -962,7 +964,9 @@ getName.call( obj ); // "foo"
|
||||||
arbitrary object @var{obj}, we can call any given method (in this case,
|
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
|
@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
|
ease.js does with each method call. To understand this process, we have to
|
||||||
explore two concepts: the visibility object itself and method wrapping.
|
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
|
@menu
|
||||||
* Visibility Object Implementation:: Design of the visibility object
|
* Visibility Object Implementation:: Design of the visibility object
|
||||||
|
@ -1107,7 +1111,8 @@ indexed by an internal class identifier (which is simply incremented for each
|
||||||
new class definition, much like we did with the instance id in
|
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
|
@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
|
that matches the class identifier associated with the invoked method is then
|
||||||
passed as the context (bound to @var{this}) for that method.
|
passed as the context (bound to @var{this}) for that method (@pxref{Method
|
||||||
|
Wrapping}).
|
||||||
|
|
||||||
@node Property Proxies
|
@node Property Proxies
|
||||||
@subsubsection Property Proxies
|
@subsubsection Property Proxies
|
||||||
|
@ -1286,6 +1291,143 @@ 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
|
the access of private properties from within classes, for which there will be no
|
||||||
performance penalty.
|
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.}
|
||||||
|
|
||||||
|
|
||||||
@node Internal Methods/Objects
|
@node Internal Methods/Objects
|
||||||
@section Internal Methods/Objects
|
@section Internal Methods/Objects
|
||||||
There are a number of internal methods/objects that may be useful to developers
|
There are a number of internal methods/objects that may be useful to developers
|
||||||
|
|
Loading…
Reference in New Issue