From a7a6eb8d83d2874fa03413bd28e54871a8b2e0cd Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Sat, 18 Feb 2012 19:58:25 -0500 Subject: [PATCH] Completed Constructor/Prototype Factory section --- coope.sty | 3 +- lst/ctor-factory-sextend.js | 4 + lst/ctor-factory.js | 103 ++++++++++++ lst/new-global-fix.js | 6 + lst/new-global.js | 12 ++ sec/encap-hacks.tex | 301 ++++++++++++++++++++++++++++-------- sec/hacking-proto.tex | 2 + 7 files changed, 362 insertions(+), 69 deletions(-) create mode 100644 lst/ctor-factory-sextend.js create mode 100644 lst/ctor-factory.js create mode 100644 lst/new-global-fix.js create mode 100644 lst/new-global.js diff --git a/coope.sty b/coope.sty index 67cd6d7..6ff406d 100644 --- a/coope.sty +++ b/coope.sty @@ -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]{//}, diff --git a/lst/ctor-factory-sextend.js b/lst/ctor-factory-sextend.js new file mode 100644 index 0000000..fae9548 --- /dev/null +++ b/lst/ctor-factory-sextend.js @@ -0,0 +1,4 @@ + ctor.extend = function( dfn ) + { + C.extend.call( this, dfn ); + }; diff --git a/lst/ctor-factory.js b/lst/ctor-factory.js new file mode 100644 index 0000000..de63fc3 --- /dev/null +++ b/lst/ctor-factory.js @@ -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 diff --git a/lst/new-global-fix.js b/lst/new-global-fix.js new file mode 100644 index 0000000..1e97295 --- /dev/null +++ b/lst/new-global-fix.js @@ -0,0 +1,6 @@ + if ( !( this instanceof ctor ) ) + { + return new ctor.apply( + null, arguments + ); + } diff --git a/lst/new-global.js b/lst/new-global.js new file mode 100644 index 0000000..5e51910 --- /dev/null +++ b/lst/new-global.js @@ -0,0 +1,12 @@ +function Foo() +{ + this.Boolean = true; +} + +// ok +var inst = new Foo(); +inst.Boolean; // true + +// bad +Foo(); +new Boolean(); // TypeError diff --git a/sec/encap-hacks.tex b/sec/encap-hacks.tex index 55c3b3c..df5fd70 100644 --- a/sec/encap-hacks.tex +++ b/sec/encap-hacks.tex @@ -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} diff --git a/sec/hacking-proto.tex b/sec/hacking-proto.tex index 683cad9..3baaae3 100644 --- a/sec/hacking-proto.tex +++ b/sec/hacking-proto.tex @@ -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