Completed Constructor/Prototype Factory section

master
Mike Gerwitz 2012-02-18 19:58:25 -05:00
parent 0f48d58072
commit a7a6eb8d83
7 changed files with 362 additions and 69 deletions

View File

@ -25,7 +25,8 @@
keywords={%
undefined,null,true,false,NaN,Infinity,return,%
try,catch,finally,function,var,if,then,else,%
in,while,do,done,case,break,continue%
in,while,do,done,case,break,continue,
new,typeof%operators
},
keywordstyle=\color{gray},
comment=[l]{//},

View File

@ -0,0 +1,4 @@
ctor.extend = function( dfn )
{
C.extend.call( this, dfn );
};

103
lst/ctor-factory.js 100644
View File

@ -0,0 +1,103 @@
var Class = ( function( extending )
{
var C = function( dfn )
{
// extend from an empty base
return C.extend( null, dfn );
};
C.extend = function( base, dfn )
{
base = base || function() {};
// prevent ctor invocation
extending = true;
var ctor = function()
{
// do nothing if extending
if ( extending )
{
return;
}
// call "actual" constructor
this.__construct &&
this.__construct.apply(
this, arguments
);
};
ctor.prototype = new base();
ctor.prototype.constructor = ctor;
copyTo( ctor.prototype, dfn );
// done extending; clear flag to
// ensure ctor can be invoked
extending = false;
return ctor;
};
function copyTo( dest, members )
{
var hasOwn = Object.prototype
.hasOwnProperty;
for ( var member in members )
{
if ( !hasOwn.call( members, member ) )
{
continue;
}
dest[ member ] = members[ member ];
}
};
return C;
} )( false );
var Foo = Class(
{
__construct: function( name )
{
if ( !name )
{
throw TypeError( "Name required" );
}
this._name = ''+( name );
},
getName: function()
{
return this._name;
}
} );
var SubFoo = Class.extend( Foo,
{
setName: function( name )
{
this._name = ''+( name );
}
} );
var myfoo = new Foo( "Foo" ),
mysub = new SubFoo( "SubFoo" );
myfoo.getName(); // "Foo"
mysub.getName(); // "SubFoo"
mysub.setName( "New Name" );
mysub.getName(); // "New Name"
// parent Foo does not define setName()
myfoo.setName( "Error" ); // TypeError
// our name will be required, since we
// are not extending
new Foo(); // TypeError

View File

@ -0,0 +1,6 @@
if ( !( this instanceof ctor ) )
{
return new ctor.apply(
null, arguments
);
}

12
lst/new-global.js 100644
View File

@ -0,0 +1,12 @@
function Foo()
{
this.Boolean = true;
}
// ok
var inst = new Foo();
inst.Boolean; // true
// bad
Foo();
new Boolean(); // TypeError

View File

@ -1,4 +1,5 @@
\section{Encapsulating the Hacks}
\label{sec:encap}
Imagine jumping into a project in order to make a simple modification and then
seeing the code in \jsref{lst:prot-share}. This is a far cry from the simple
protected member declarations in traditional classical object-oriented
@ -25,7 +26,7 @@ discussed a bit later.
\subsection{Constructor/Prototype Factory}
\label{sec:ctor-factory}
Section~\ref{sec:extending} offered one solution to the problem of creating a an
Section~\ref{sec:extending} offered one solution to the problem of creating an
extensible constructor, allowing it to be used both to instantiate new objects
and as a prototype. Unfortunately, as \jsref{lst:ctor-extend} demonstrated, the
solution adds a bit of noise to the definition that will also be duplicated for
@ -33,7 +34,7 @@ each constructor. The section ended with the promise of a cleaner, reusable
implementation. Perhaps we can provide that.
Consider once again the issue at hand. The constructor, when called
conventionally with the \keyword{new} keyword to create a new instance, must
conventionally with the \operator{new} operator to create a new instance, must
perform all of its construction logic. However, if we wish to use it as a
prototype, it is unlikely that we want to run \emph{any} of that logic --- we
are simply looking to have an object containing each of its members to use as a
@ -46,86 +47,250 @@ ourselves. We cannot use the name \func{extend()}, as it is a reserved
name;\footnote{Perhaps for future versions of ECMAScript.} as such, we will
start with a simple \func{Class} factory function with which we can create new
``classes'' without supertypes. We can than provide a \func{Class.extend()}
method to define a ``class'' with a supertype.
method to define a ``class'' \emph{with} a supertype.
\begin{lstlisting}[%
\lstinputlisting[%
label=lst:ctor-factory,
caption=Constructor factory
]
var Class = ( function( extending )
caption=Constructor factory,
lastline=60,
]{lst/ctor-factory.js}
\jsref{lst:ctor-factory} demonstrates one such possible implementation of a
constructor factory. Rather than thinking of ``creating a class'' and ``creating
a class with a supertype'' as two separate processes, it is helpful to consider
them one and the same; instead, we can consider the former to be ``creating a
class \emph{with an empty supertype}''. As such, invoking \func{Class()} simply
calls \func{Class.extend()} with \keyword{null} for the base (on line 6),
allowing \func{Class.extend()} to handle the creation of a new constructor
without a supertype.
Both \func{Class()} and \func{Class.extend()} accept a \var{dfn} argument, which
we will refer to as the \dfn{definition object}; this object is to contain each
member that will appear on the prototype of the new constructor. The \var{base}
parameter, defined on \func{Class.extend()}, denotes the constructor from which
to extend (the constructor that will be instantiated and used as the prototype).
Line 11 will default \var{base} to an empty function if one has not been
provided (mainly, to satisfy the \func{Class()} call on line 6).
With that, we can now continue onto creating our constructor, beginning on line
16. Section~\ref{sec:extending} introduced the concept of using an
\var{extending} flag to let the constructor know when to avoid all of its
construction logic if being used only as a prototype (see
\jsref{lst:ctor-extend}). The problem with this implementation, as discussed,
was that it required that \emph{each} constructor that wishes to use this
pattern implement it themselves, violating the DRY\footnote{``Don't repreat
yourself'', \emph{The Pragmatic Programmer}.} principle. There were two main
areas of code duplication in \jsref{lst:ctor-extend} --- the checking of the
\var{extending} flag in the constructor and the setting (and resetting) of the
flag in \func{F.asPrototype()}. In fact, we can eliminate the
\func{asPrototype()} method altogether once we recognize that its entire
purpose is to set the flags before and after instantiation.
To address the first code duplication issue --- the checking of the flag in the
constructor --- we must remove the need to perform the check manually for each
and every constructor. The solution, as demonstrated in
\jsref{lst:ctor-factory}, is to separate our generic constructor logic (shared
between all constructors that use the factory) from the logic that can vary
between each constructor. \var{ctor} on line 16 accomplishes this by first
performing the \var{extending} check (lines 19--22) and then forwarding all
arguments to a separate function (\func{\_\_construct()}), if defined, using
\func{Function.apply()} (lines 25--28). One could adopt any name for the
constructor method; it is not significant.\footnote{The \code{\_\_construct}
name was taken from PHP.} Note that the first argument to
\func{Function.apply()} is important, as it will ensure that \keyword{this} is
properly bound within the \func{\_\_construct()} method.
To address the second code duplication issue and remove the need for
\func{asPrototype()} in \jsref{lst:ctor-extend} entirely, we can take advantage
of the implications of \func{Class.extend()} in \jsref{lst:ctor-factory}. The
only time we wish to use a constructor as a prototype and skip
\func{\_\_construct()} is during the process of creating a new constructor. As
such, we can simply set the \var{extending} flag to \keyword{true} when we begin
creating the new constructor (see line 14, though this flag could be placed
anywhere before line 31) and then reset it to \keyword{false} once we are done
(line 38). With that, we have eliminated the code duplication issues associated
with \jsref{lst:ctor-extend}.
The remainder of \jsref{lst:ctor-factory} is simply an abstraction around the
manual process we have been performing since section~\ref{sec:proto} --- setting
the prototype, properly setting the constructor and extending the prototype
with our own methods. Recall section~\ref{sec:prot} in which we had to manually
assign each member of the prototype for subtypes in order to ensure that we did
not overwrite the existing prototype members (e.g. \func{M.prototype.push()} in
\jsref{lst:prot-share}). The very same issue applies here: Line 31 first sets
the prototype to an instance of \var{base}. If we were to then set
\code{ctor.prototype = dfn}, we would entirely overwrite the benefit gained from
specifying \var{base}. In order to automate this manual assignment of each
additional prototype member of \var{dfn}, \func{copyTo()} is provided, which
accepts two arguments --- a destination object \var{dest} to which each given
member of \var{members} should be copied (defined on line 43 and called on line
34).
Like the examples provided in section~\ref{sec:hack-around}, we
use a self-executing function to hide the implementation details of our
\func{Class} function from the rest of the world.
To demonstrate use of the constructor factory, \jsref{lst:ctor-factory-ex}
defines two classes\footnote{The reader should take care in noting that the term
``class'', as used henceforth, will refer to a class-like object created using
the systems defined within this article. ECMAScript does not support classes, so
the use of the term ``class'' in any other context is misleading.} --- \var{Foo}
and \var{SubFoo}. Note that how, by placing the curly braces on their own line,
we can create the illusion that \func{Class()} is a language construct:
\lstinputlisting[%
label=lst:ctor-factory-ex,
caption=Demonstrating the constructor factory,
firstline=62,
firstnumber=last
]{lst/ctor-factory.js}
The reader should note that an important assertion has been omitted for brevity
in \jsref{lst:ctor-factory}. Consider, for example, what may happen in the case
of the following:
\begin{verbatim}
Class.extend( "foo", {} );
\end{verbatim}
It is apparent that \code{"foo"} is not a function and therefore cannot be used
with the \keyword{new} keyword. Given that, consider line 31, which blindly
invokes \code{base()} without consideration for the very probable scenario that
the user mistakenly (due to their own unfamiliarity or a simple bug) provided us
with a non-constructor for \var{base}. The user would then be presented with a
valid, but not necessarily useful error --- did the error occur because of user
error, or due to a bug in the factory implementation?
To avoid confusion, it would be best to perform a simple assertion before
invoking \var{base} (or wrap the invocation in a try/catch block, although doing
so is not recommended in case \func{base()} throws an error of its own):
\begin{verbatim}
if ( typeof base !== 'function' )
{
var C = function( dfn )
{
// extend from an empty base
return C.extend( null, dfn );
};
throw TypeError( "Invalid base provided" );
}
\end{verbatim}
C.extend = function( base, dfn )
{
base = base || function() {};
Note also that, although this implementation will work with any constructor as
\var{base}, only those created with \func{Class()} will have the benefit of
being able to check the \var{extending} flag. As such, when using
\func{Class.extend()} with third-party constructors, the issue of extensible
constructors may still remain and is left instead in the hands of the developer
of that base constructor.
var ctor = function()
{
// do nothing if extending
if ( extending )
{
return;
}
\subsubsection{Factory Conveniences}
Although our constructor factory described in section~\ref{sec:ctor-factory} is
thus far very simple, one should take the time to realize what a powerful
abstraction has been created; it allows us to inject our own code in any part of
the constructor creation process, giving us full control over our class-like
objects. Indeed, this abstraction will be used as a strong foundation going
forward throughout all of section~\ref{sec:encap}. In the meantime, we can take
advantage of it in its infancy to provide a couple additional conveniences.
// call "actual" constructor
this.__construct &&
this.__construct.apply(
this, arguments
);
};
First, consider the syntax of \func{Class.extend()} in \jsref{lst:ctor-factory}.
It requires the extending of a constructor to be done in the following manner:
ctor.prototype = new base();
ctor.prototype.constructor = ctor;
\begin{verbatim}
var SubFoo = Class.extend( Foo, {} );
\end{verbatim}
copyTo( ctor.prototype, dfn );
Would it not be more intuitive to instead be able to extend a constructor in the
following manner?
return ctor;
};
\begin{verbatim}
var SubFoo = Foo.extend( {} );
\end{verbatim}
function copyTo( dest, members )
{
var hasOwn = Object.prototype
.hasOwnProperty;
The above two statements are semantically equivalent --- they define a subtype
\var{SubFoo} that extends from the constructor \var{Foo} --- but the latter
example is more concise and natural. Adding support for this method is trivial,
involving only a slight addition to \jsref{sec:ctor-factory}'s \func{C.extend()}
method, perhaps around line 30:
for ( var member in members )
{
if ( !hasOwn.call( members, member ) )
{
continue;
}
\lstinputlisting[%
label=lst:ctor-factory-sextend,
caption=Adding a static \func{extend()} method to constructors,
firstnumber=31
]{lst/ctor-factory-sextend.js}
dest[ member ] = members[ member ];
}
};
Of course, one should be aware that this implementation is exploitable in that,
for example, \func{Foo.extend()} could be reassigned at any point. As such,
using \func{Class.extend()} is the safe implementation, unless you can be
certain that such a reassignment is not possible. Alternatively, in ECMAScript 5
and later environments, one can use \func{Object.defineProperty()}, as discussed
in sections~\ref{sec:encap-naive} and \ref{sec:encap-proper}, to make the method
read-only.
return C;
} )( false );
Now consider the instantiation of our class-like objects, as was demonstrated in
\jsref{lst:ctor-factory-ex}:
var Foo = Class(
{
__construct: function( name, ignore )
{
ignore || throw Error( "Ctor called" );
this._name = ''+( name );
},
\begin{verbatim}
var inst = new Foo( "Name" );
\end{verbatim}
getName: function()
{
return this._name;
}
} );
We can make our code even more concise by eliminating the \keyword{new} keyword
entirely, allowing us to create a new instance as such:
var SubFoo = Class.extend( Foo,
{
setName: function( name )
{
this._name = ''+( name );
}
} );
\begin{verbatim}
var inst = Foo( "Name" );
\end{verbatim}
Of course, our constructors do not yet support this, but why may we want such a
thing? Firstly, for consistency --- the core ECMAScript constructors do not
require the use of the keyword, as has been demonstrated throughout this article
with the various \var{Error} types. Secondly, the omission of the keyword would
allow us to jump immediately into calling a method on an object without dealing
with awkward precedence rules: \code{Foo( "Name" ).getName()} vs. \code{( new
Foo( "Name" ) ).getName()}. However, those reasons exit more to offer syntactic
sugar; they do little to persuade those who do want or not mind the
\keyword{new} keyword.
The stronger argument against the \keyword{new} keyword is what happens should
someone \emph{omit} it, which would not be at all uncommon since the keyword is
not required for the core ECMAScript constructors. Recall that \keyword{this},
from within the constructor, is bound to the new instance when invoked with the
\keyword{new} keyword. As such, we expect to be able to make assignments to
properties of \keyword{this} from within the constructor without any problems.
What, then, happens if the constructor is invoked \emph{without} the keyword?
\keyword{this} would instead be bound (according to the ECMAScript
standard\footnote{See ECMAScript Language Specification, ECMA-262 5.1 Edition,
Section 1.4.3 on pg 58.}) to ``the global object'',\footnote{In most browser
environments, the global object is \var{window}.} unless in strict mode. This is
dangerous:
\lstinputlisting[%
label=lst:new-global,
caption=Introducing unintended global side-effects with constructors
]{lst/new-global.js}
Consider \jsref{lst:new-global} above. Function \func{Foo()}, if invoked with
the \keyword{new} keyword, results in an object with a \var{Boolean} property
equal to \keyword{true}. However, if we were to invoke \func{Foo()}
\emph{without} the \keyword{new} keyword, this would end up \emph{overwriting
the built-in global \var{Boolean} object reference}. To solve this problem,
while at the same time providing the consistency and convenience of being able
to either include or omit the \keyword{new} keyword, we can add a small block of
code to our generated constructor \var{ctor} (somewhere around line 23 of
\jsref{lst:ctor-factory}, after the extend check but before
\func{\_\_construct()} is invoked):
\lstinputlisting[%
label=lst:new-global-fix,
caption=Allowing for omission of the \keyword{new} keyword,
firstnumber=24
]{lst/new-global-fix.js}
The check, as demonstrated in \jsref{lst:new-global-fix}, is as simple as
ensuring that \keyword{this} is properly bound to a \emph{new instance of our
constructor \var{ctor}}. If not, the constructor can simply return a new
instance of itself through a recursive call.
Alternatively, the reader may decide to throw an error instead of automatically
returning a new instance. This would require the use of the \keyword{new}
keyword, while still ensuring the global scope will not be polluted with
unnecessary values. If the constructor is in strict mode, then the error would
help to point out bugs in the code. However, for the reason that the keyword is
optional for many core ECMAScript constructors, the author recommends the
implementation in \jsref{lst:new-global-fix}.
\end{lstlisting}

View File

@ -1,4 +1,5 @@
\section{Hacking Around Prototypal Limitations}
\label{sec:hack-around}
Section~\ref{sec:class-like} demonstrated how one would work within the
limitations of conventional ECMAScript to produce class-like objects using
prototypes. For those coming from other classical object-oriented languages,
@ -582,6 +583,7 @@ Since private members needn't be inherited by subtypes, no additional work needs
to be done.
\subsection{Protected Members}
\label{sec:prot}
We have thus far covered two of the three access modifiers (see
section~\ref{sec:encap}) --- public and private. Those implementations allowed
us to remain blissfully ignorant of inheritance, as public members are handled