558 lines
25 KiB
TeX
558 lines
25 KiB
TeX
\section{Class-Like Objects in ECMAScript}
|
|
\label{sec:class-like}
|
|
JavaScript is a multi-paradigm scripting language standardized by ECMAScript,
|
|
incorporating object-oriented, functional and imperative styles. The
|
|
Object-Oriented paradigm in itself supports two sub-paradigms - prototypal and
|
|
classical, the latter of which is popular in languages such as Java, C++,
|
|
Python, Perl, Ruby, Lisp, PHP, Smalltalk, among many others. ECMAScript itself
|
|
is prototypal.
|
|
|
|
The creation of objects in ECMAScript can be as simple as using an object
|
|
literal, as defined by curly braces:
|
|
|
|
\begin{verbatim}
|
|
var obj = { foo: "bar" };
|
|
\end{verbatim}
|
|
|
|
In a classical sense, object literals can be thought of as anonymous
|
|
singletons; \cite{gof} that is, they have no name (they are identified by
|
|
the variable to which they are assigned) and only one instance of the literal
|
|
will exist throughout the life of the software.\footnote{Technically, one could
|
|
set the prototype of a constructor to be the object defined by the literal (see
|
|
\jsref{lst:proto-reuse}), however the resulting instances would be prototypes,
|
|
not instances of a common class shared by the literal and each subsequent
|
|
instance.} For example, calling a function that returns the same object literal
|
|
will return a distinct, entirely unrelated object for each invocation:
|
|
|
|
\begin{verbatim}
|
|
function createObj()
|
|
{
|
|
return { name: "foo" };
|
|
}
|
|
|
|
createObj() !== createObj();
|
|
\end{verbatim}
|
|
|
|
Using this method, we can create basic objects that act much like class
|
|
instances, as demonstrated in \jsref{lst:singleton}:
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:singleton,
|
|
caption=A ``singleton'' with properties and methods
|
|
]
|
|
var obj = {
|
|
name: "Foo",
|
|
|
|
setName: function( val )
|
|
{
|
|
obj.name = val;
|
|
},
|
|
|
|
getName: function()
|
|
{
|
|
return obj.name;
|
|
}
|
|
};
|
|
|
|
obj.getName(); // "Foo"
|
|
obj.setName( "Bar" );
|
|
obj.getName(); // "Bar"
|
|
\end{lstlisting}
|
|
|
|
\subsection{Prototypes}
|
|
\label{sec:proto}
|
|
We could re-use \var{obj} in \jsref{lst:singleton} as a \dfn{prototype},
|
|
allowing instances to inherit its members. For example:
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:proto-reuse,
|
|
caption=Re-using objects as prototyes \bf{(bad)},
|
|
firstnumber=last
|
|
]
|
|
function Foo() {}
|
|
Foo.prototype = obj;
|
|
|
|
var inst1 = new Foo(),
|
|
inst2 = new Foo();
|
|
|
|
inst2.setName( "Bar" );
|
|
|
|
inst1.getName(); // "Bar"
|
|
inst2.getName(); // "Bar"
|
|
\end{lstlisting}
|
|
|
|
In \jsref{lst:proto-reuse} above, we define \var{Foo} to be a
|
|
\dfn{constructor}\footnote{A ``constructor'' in ECMAScript is simply any
|
|
function intended to be invoked, often (but not always) with the \operator{new}
|
|
operator, that returns a new object whose members are derived from the
|
|
function's \var{prototype} property.} with our previous object \var{obj} as its
|
|
prototype. Unfortunately, as shown in \jsref{lst:singleton}, \var{name} is being
|
|
set on \var{obj} itself, which is a prototype shared between both instances.
|
|
Setting the name on one object therefore changes the name on the other (and,
|
|
indeed, all instances of \var{Foo}). To illustrate this important concept,
|
|
consider \jsref{lst:proto-mod} below, which continues from
|
|
\jsref{lst:proto-reuse}:
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:proto-mod,
|
|
caption=The effect of prototypes on instances,
|
|
firstnumber=last
|
|
]
|
|
obj.foo = "bar";
|
|
inst1.foo; // "bar"
|
|
inst2.foo; // "bar"
|
|
\end{lstlisting}
|
|
|
|
Clearly, this is not how one would expect class-like objects to interact; each
|
|
object is expected to have its own state. When accessing a property of an
|
|
object, the members of the object itself are first checked. If the member
|
|
is not defined on the object itself,\footnote{Note that ``not defined'' does not
|
|
imply \emph{undefined}; \code{undefined} is a value.} then the prototype chain
|
|
is traversed. Therefore, we can give objects their own individual state by
|
|
defining the property on the individual instances, rather than the prototype, as
|
|
shown in \jsref{lst:inst-prop}.\footnote{Also demonstrated in
|
|
\jsref{lst:inst-prop} is the effect of the \keyword{delete} keyword, which
|
|
removes a member from an object, allowing the values of the prototype to ``peek
|
|
through`` as if a hole exists in the object. Setting the value to
|
|
\code{undefined} will not have the same effect, as it does not produce the
|
|
``hole''; the property would return \code{undefined} rather than the value on
|
|
the prototype.}
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:inst-prop,
|
|
caption=Setting properties per-instance,
|
|
firstnumber=last
|
|
]
|
|
inst1.foo = "baz";
|
|
inst1.foo; // "baz"
|
|
inst2.foo; // "bar"
|
|
|
|
delete inst1.foo;
|
|
inst1.foo; // "bar"
|
|
\end{lstlisting}
|
|
|
|
This does not entirely solve our problem. As shown in \jsref{lst:singleton}, our
|
|
\var{obj} prototype's methods (\func{getName()} and \func{setName()}) reference
|
|
\code{obj.name} - our prototype. \jsref{lst:proto-ref} demonstrates the problem
|
|
this causes when attempting to give each instance its own state in regards to
|
|
the \var{name} property:
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:proto-ref,
|
|
caption=Referencing prototype values in \var{obj} causes problems with
|
|
per-instance data,
|
|
firstnumber=last
|
|
]
|
|
// ...
|
|
|
|
inst1.name = "My Name";
|
|
inst1.getName(); // "Foo"
|
|
\end{lstlisting}
|
|
|
|
ECMAScript solves this issue with the \keyword{this} keyword. When a
|
|
method\footnote{A \dfn{method} is simply an invokable property of an object (a
|
|
function).} of an instance's prototype is invoked, \keyword{this} is bound, by
|
|
default,\footnote{One can override this default behavior with
|
|
\func{Function.call()} or \func{Function.apply()}.} to a reference of that
|
|
instance. Therefore, we can replace \var{obj} in \jsref{lst:singleton} with the
|
|
prototype definition in \jsref{lst:proto-proper} to solve the issue
|
|
demonstrated in \jsref{lst:proto-ref}:
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:proto-proper,
|
|
caption=Re-using objects as prototypes \bf{(good)}
|
|
]
|
|
function Foo( name )
|
|
{
|
|
this.name = name;
|
|
};
|
|
|
|
Foo.prototype = {
|
|
setName = function( name )
|
|
{
|
|
this.name = name;
|
|
},
|
|
|
|
getName = function()
|
|
{
|
|
return this.name;
|
|
}
|
|
};
|
|
|
|
var inst = new Foo( "Bar" );
|
|
inst.name; // "Bar"
|
|
inst.getName(); // "Bar"
|
|
|
|
inst.setName( "Baz" );
|
|
inst.getName(); // "Baz"
|
|
|
|
inst.name = "Foo";
|
|
inst.getName(); // "Foo"
|
|
\end{lstlisting}
|
|
|
|
\jsref{lst:proto-proper} shows that \keyword{this} is also bound to the new
|
|
instance from within the constructor; this allows us to initialize any
|
|
properties on the new instance before it is returned to the caller.\footnote{It
|
|
is worth mentioning that one can explicitly return an object from the
|
|
constructor, which will be returned in place of a new instance.} Evaluation of
|
|
the example yields an additional concern --- the observation that all object
|
|
members in ECMAScript are public.\footnote{That is not to say that encapsulation
|
|
is not possible; this statement is merely emphasizing that properties of objects
|
|
do not support access modifiers. We will get into the topic of encapsulation a
|
|
bit later.} Even though the \var{name} property was initialized within the
|
|
constructor, it is still accessible outside of both the constructor and the
|
|
prototype. Addressing this concern will prove to be an arduous process that
|
|
will be covered at great length in the following sections. For the time being,
|
|
we will continue discussion of conventional techniques, bringing us to the
|
|
concept of \dfn{privileged members}.
|
|
|
|
\subsection{Privileged Members}
|
|
\label{sec:privileged}
|
|
The concept of \dfn{encapsulation} is a cornerstone of classical object-oriented
|
|
programming. Unfortunately, as \jsref{lst:proto-proper} demonstrates, it becomes
|
|
difficult to encapsulate data if all members of a given object are accessible
|
|
publicly. One means of addressing this issue is to take advantage of the fact
|
|
that functions introduce scope, allowing us to define a local variable (or use
|
|
an argument) within the constructor that is only accessible to the
|
|
\dfn{privileged member} \func{getName()}.
|
|
|
|
\lstinputlisting[%
|
|
label=lst:privileged,
|
|
caption=Using privileged members to encapsulate data
|
|
]{lst/privileged-members.js}
|
|
|
|
If \var{name} in \jsref{lst:privileged} is encapsulated within the constructor,
|
|
our methods that \emph{access} that encapsulated data must \emph{too} be
|
|
declared within the constructor;\footnote{One may mix prototypes and privileged
|
|
members.} otherwise, if placed within the prototype, \var{name} would be out of
|
|
scope. This implementation has an unfortunate consequence --- our methods are
|
|
now being \emph{redeclared} each and every time a new instance of \var{Foo} is
|
|
created, which has obvious performance penalties (see
|
|
\fref{fig:proto-priv-cmp}).\footnote{As a general rule of thumb, one should only
|
|
use privileged members for methods that access encapsulated data; all other
|
|
members should be part of the prototype.}
|
|
|
|
\begin{figure}
|
|
\center
|
|
\begin{tabular}{r|r|r|r|}
|
|
\cline{2-4}
|
|
& Heap Usage
|
|
& \multicolumn{1}{|c|}{Inst. Time}
|
|
& \multicolumn{1}{|c|}{Call Time} \\
|
|
\hline
|
|
\multicolumn{1}{|r|}{\jsref{lst:proto-proper}}
|
|
& 49.7M & 234ms & 17ms \\
|
|
\multicolumn{1}{|r|}{\jsref{lst:privileged}}
|
|
& 236.0M & 1134ms & 28ms \\
|
|
\hline
|
|
\multicolumn{1}{|r|}{\% Change} & 374.8\% & 384.6\% & 64.7\% \\
|
|
\hline
|
|
\end{tabular}
|
|
\caption{Comparing performance of privileged member and prototype
|
|
implementations under v8. The heap usage column represents the
|
|
heap usage after instantiating \var{Foo} under the respective implementation $n$
|
|
times, and the Inst. CPU column reflects the amount of time spent instantiating
|
|
the $n$ objects. The Call CPU column reflects the amount of time spent invoking
|
|
\emph{each} member of \emph{one} instance $n$ times. $n = 1,000,000$. Lower
|
|
numbers are better. Different environments may have different results.}
|
|
\label{fig:proto-priv-cmp}
|
|
\end{figure}
|
|
|
|
Due to these performance concerns, it is often undesirable to use privileged
|
|
members; many developers will instead prefix, with an underscore, members
|
|
intended to be private (e.g. \code{this.\_name}) while keeping all methods on
|
|
the prototype.\footnote{One example of a library that uses underscores in place
|
|
of privileged members is Dojo at http://dojotoolkit.org.} This serves as a clear
|
|
indicator that the API is not public, is subject to change in the future and
|
|
should not be touched. It also allows the property to be accessed by
|
|
subtypes,\footnote{The term ``subtype'' is not truly the correct term here.
|
|
Rather, the term in this context was meant to imply that an instance of the
|
|
constructor was used as the prototype for another constructor, acting much like
|
|
a subtype (child class).} acting like a protected member. Unfortunately, this
|
|
does not encapsulate the data, so the developer must trust that the user will
|
|
not tamper with it.
|
|
|
|
\subsection{Subtypes and Polymorphism}
|
|
In classical terms, \dfn{subtyping} (also known as \dfn{subclassing}) is the act of
|
|
extending a \dfn{supertype} (creating a \dfn{child} class from a \dfn{parent})
|
|
with additional functionality. The subtype is said to \dfn{inherit} its members
|
|
from the supertype.\footnote{In the case of languages that support access
|
|
modifiers, only public and protected members are inherited.} Based on our prior
|
|
examples in section~\ref{sec:proto}, one could clearly see how the prototype of
|
|
any constructor could be replaced with an instance of another constructor,
|
|
indefinitely, to achieve an inheritance-like effect. This useful consequence of
|
|
the prototype model is demonstrated in \jsref{lst:subtype}.\footnote{Unfortunately, a
|
|
responsible implementation is not all so elegant in practice.}
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:subtype,
|
|
caption=Extending prototypes (creating subtypes) in ECMAScript
|
|
]
|
|
var SubFoo = function( name )
|
|
{
|
|
// call parent constructor
|
|
Foo.call( this, name );
|
|
};
|
|
SubFoo.prototype = new Foo();
|
|
SubFoo.prototype.constructor = SubFoo;
|
|
|
|
// build upon (extend) Foo
|
|
SubFoo.prototype.hello = function()
|
|
{
|
|
return "Hello, " + this.name;
|
|
};
|
|
|
|
var inst = new SubFoo( "John" );
|
|
inst.getName(); // "John"
|
|
inst.hello(); // "Hello, John"
|
|
\end{lstlisting}
|
|
|
|
Consider the implications of \jsref{lst:subtype} with a close eye. This
|
|
extension of \var{Foo} is rather verbose. The first (and rather unpleasant fact
|
|
that may be terribly confusing to those fairly inexperienced with ECMAScript)
|
|
consideration to be made is \var{SubFoo}'s constructor. Note how the supertype
|
|
(\var{Foo}) must be invoked \emph{within the context of
|
|
\var{SubFoo}}\footnote{If \func{Function.call()} or \func{Function.apply()} are
|
|
not properly used, the function will, depending on the environment, assign
|
|
\keyword{this} to the global scope, which is absolutely not what one wants. In
|
|
strict mode, this effect is mitigated, but the result is still not what we
|
|
want.} in order to initialize the variables.\footnote{If the constructor accepts
|
|
more than a few arguments, one could simply do: \code{Foo.apply( this, arguments
|
|
);}} However, once properly deciphered, this call is very similar to invocation
|
|
of parent constructors in other languages.
|
|
|
|
Following the definition of \var{SubFoo} is its prototype (line 6). Note from
|
|
section~\ref{sec:proto} that the prototype must contain the members that are to
|
|
be accessible to any instances of the constructor. If we were to simply assign
|
|
\var{Foo} to the prototype, this would have two terrible consequences, the
|
|
second of which will be discussed shortly. The first consequence would be that
|
|
all members of \var{Foo} \emph{itself} would be made available to instances of
|
|
\var{SubFoo}. In particular, you would find that \code{( new SubFoo()
|
|
).prototype === Foo.prototype}, which is hardly your intent. As such, we must
|
|
use a new instance of \var{Foo} for our prototype, so that the prototype
|
|
contains the appropriate members.
|
|
|
|
We follow the prototype assignment with another alien declaration --- the
|
|
setting of \code{SubFoo.prototype.constructor} on line 7. To understand why this
|
|
is necessary, one must first understand that, given any object \var{o} such that
|
|
\code{var o = new O()}, \code{o.constructor === O}.\footnote{One could apply
|
|
this same concept to other core ECMAScript objects. For example, \code{(
|
|
function() \{\}).constructor === Function}, \code{[].constructor === Array},
|
|
\code{\{\}.constructor === Object}, \code{true.constructor === Boolean} and
|
|
sofourth.} Recall from section~\ref{sec:proto} that values ``peek through
|
|
holes'' in the prototype chain. In this case, without our intervention,
|
|
\code{SubFoo.prototype.constructor === Foo} because \code{SubFoo.prototype = new
|
|
Foo()}. The \var{constructor} property is useful for reflection, so it is
|
|
important that we properly set this value to the appropriate constructor ---
|
|
\var{SubFoo}. Since \var{SubFoo.prototype} is an \emph{instance} of \var{Foo}
|
|
rather than \var{Foo} itself, the assignment will not directly affect \var{Foo}.
|
|
This brings us to our aforementioned second consequence of assigning
|
|
\code{SubFoo.prototype} to a \emph{new} instance of \var{Foo} --- extending the
|
|
prototype by adding to or altering existing values would otherwise change the
|
|
supertype's constructor, which would be an unintentional side-effect
|
|
that could have drastic consequences on the software.
|
|
|
|
As an example of extending the prototype (we have already demonstrated
|
|
overwriting the \var{constructor} and this concept can be applied to overriding
|
|
any members of the supertype), method \var{hello()} has been included in
|
|
\jsref{lst:subtype} on line 10. Note that \keyword{this} will be bound to the
|
|
instance that the method is being invoked upon, since it is referenced within
|
|
the prototype. Also note that we are assigning the function in a slightly
|
|
different manner than in \jsref{lst:proto-proper}; this is necessary to ensure
|
|
that we do not overwrite the prototype we just declared. Any additional members
|
|
must be declared explicitly in this manner, which has the negative consequence
|
|
of further increasing the verbosity of the code.
|
|
|
|
An instance of a subtype can be used in place of any of its supertypes in a
|
|
concept known as \dfn{polymorphism}. \jsref{lst:poly} demonstrates this concept
|
|
with \func{getFooName()}, a function that will return the name of any object of
|
|
type \var{Foo}.\footnote{Please note that the \operator{typeof} operator is not
|
|
appropriate in this situation, as both instances of \var{Foo} and \var{SubFoo}
|
|
would be considered typeof ``object''. The \operator{instanceof} operator is
|
|
appropriate when determining types of objects in terms of their
|
|
constructor.}
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:poly,
|
|
caption=Polymorphism in ECMAScript
|
|
]
|
|
function getFooName( foo )
|
|
{
|
|
if ( !( foo instanceof Foo ) )
|
|
{
|
|
throw TypeError(
|
|
"Expected instance of Foo"
|
|
);
|
|
}
|
|
|
|
return foo.getName();
|
|
}
|
|
|
|
var inst_parent = new Foo( "Parent" ),
|
|
inst_child = new SubFoo( "Child" );
|
|
|
|
getFooName( inst_parent ); // "Parent"
|
|
getFooName( inst_child ); // "Child"
|
|
getFooName( {} ); // throws TypeError
|
|
\end{lstlisting}
|
|
|
|
The concepts demonstrated in this section could be easily used to extend
|
|
prototypes indefinitely, creating what is called a \dfn{prototype chain}. In the
|
|
case of an instance of \var{SubFoo}, the prototype chain of most environments
|
|
would likely be: \var{SubFoo}, \var{Foo}, \var{Object} (that is,
|
|
\code{Object.getPrototypeOf( new SubFoo() ) === SubFoo}, and so
|
|
fourth).\footnote{ECMAScript 5 introduces \code{Object.getPrototypeOf()}, which
|
|
allows retrieving the prototype of an object (instance). Some environments also
|
|
support the non-standard \var{\_\_proto\_\_} property, which is a JavaScript
|
|
extension.} Keep in mind, however, that the further down the prototype chain the
|
|
engine must traverse in order to find a given member, the greater the
|
|
performance impact.
|
|
|
|
Due to the method used to ``extend'' prototypes, it should also be apparent that
|
|
multiple inheritance is unsupported by ECMAScript, as each each constructor may
|
|
only have one \var{prototype} property.\footnote{Multiple inheritance is
|
|
well-known for its problems. As an alternative, styles of programming similar to
|
|
the use of interfaces and traits/mixins in other languages are recommended and
|
|
are possible in ECMAScript.}
|
|
|
|
\subsubsection{Extensible Constructors}
|
|
\label{sec:ext-ctor}
|
|
Before moving on from the topic of extending prototypes, the assignment of
|
|
\code{SubFoo.prototype} deserves some additional discussion. Consider the
|
|
implications of this assignment; particularity, the invocation of the
|
|
constructor \var{Foo}. ECMAScript does not perform assignments to prototypes
|
|
differently than any other assignment, meaning all the logic contained within
|
|
the constructor \var{Foo} will be executed. In our case, this does not have any
|
|
terrible consequences --- \var{name} will simply be initialized to
|
|
\code{undefined}, which will be overridden once \var{SubType} is invoked.
|
|
However, consider what may happen if \var{Foo} performed checks on its
|
|
arguments.
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:ctor-problem,
|
|
caption=Potential constructor problems for prototype assignments
|
|
]
|
|
function Foo( name )
|
|
{
|
|
if ( typeof name !== 'string' )
|
|
{
|
|
throw TypeError( "Invalid name" );
|
|
}
|
|
|
|
this.name = name;
|
|
}
|
|
|
|
// ...
|
|
SubFoo.prototype = new Foo(); // TypeError
|
|
\end{lstlisting}
|
|
|
|
As \jsref{lst:ctor-problem} shows, we can no longer use a new instance of
|
|
\var{Foo} as our prototype, unless we were to provide dummy data that will pass
|
|
any type checks and validations that the constructor performs. Dummy data is not
|
|
an ideal solution --- it muddies the code and will cause subtypes to break
|
|
should any validations be added to the supertype in the future.\footnote{Of
|
|
course, if the constructor of the supertype changes, there are always BC
|
|
(backwards-compatibility) concerns. However, in the case of validations in the
|
|
constructor, they may simply enforce already existing docblocks, which should
|
|
have already been adhered to.} Furthermore, all constructor logic will still be
|
|
performed. What if \var{Foo} were to do something considerably more intensive
|
|
--- perform vigorous data validations or initialize a database connection,
|
|
perhaps?\footnote{Constructors should take care in limiting what actions they
|
|
perform, especially if they produce side-effects.} Not only would we have to
|
|
provide potentially complicated dummy data or dummy/stubbed objects, our
|
|
prototype assignment would also incur an unnecessary performance hit. Indeed,
|
|
the construction logic would be performed \(n + 1\) times --- once for the
|
|
prototype and once for each instance, which would overwrite the results of the
|
|
previous constructor (or duplicate, depending on implementation).
|
|
|
|
How one goes about solving this problem depends on the needs of the constructor.
|
|
Let us first consider a very basic solution --- ignoring constructor logic if
|
|
the provided argument list is empty, as is demonstrated in
|
|
\jsref{lst:ctor-ignore-empty}.
|
|
|
|
\begin{lstlisting}[%
|
|
label=lst:ctor-ignore-empty,
|
|
caption=Ignoring construction logic if provided with an empty argument list
|
|
]
|
|
function Foo( name )
|
|
{
|
|
if ( arguments.length === 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// ...
|
|
|
|
this.name = name;
|
|
}
|
|
|
|
// ...
|
|
SubType.prototype = new Foo(); // OK
|
|
\end{lstlisting}
|
|
|
|
This solution has its own problems. The most apparent issue is that one could
|
|
simply omit all constructor arguments to bypass constructor logic, which is
|
|
certainly undesirable.\footnote{Constructors allow us to initialize our object,
|
|
placing it in a consistent and predictable state. Allowing a user to bypass this
|
|
logic could not only introduce unintended consequences during the life of the
|
|
object, but would mandate additional checks during method calls to ensure the
|
|
current state is sane, which will add unnecessary overhead.} Secondly --- what
|
|
if \var{Foo}'s \var{name} parameter was optional and additional construction
|
|
logic needed to be performed regardless of whether or not \var{name} was
|
|
provided? Perhaps we would want to provide a default value for \var{name} in
|
|
addition to generating a random hash that can be used to uniquely identify each
|
|
instance of \var{Foo}. If we are immediately returning from the constructor when
|
|
all arguments are omitted, then such an implementation is not possible. Another
|
|
solution is needed in this case.\footnote{That is not to say that our first
|
|
solution --- immediately returning if no arguments are provided --- is useless.
|
|
This is a commonly used method that you may find useful for certain
|
|
circumstances.}
|
|
|
|
A solution that satisfies all needs involves a more complicated hack that we
|
|
will defer to section~\ref{sec:extending}.\footnote{One may ask why, given all of
|
|
the complications of extending prototypes, one doesn't simply set
|
|
\code{SubFoo.prototype = Foo.prototype}. The reason for this is simple --- we
|
|
would not be able to extend the prototype without modifying the original, as
|
|
they would share references to the same object.}
|
|
|
|
\subsection{Shortcomings}
|
|
ECMAScript's prototype model is highly flexible, but leaves much to be desired:
|
|
|
|
\begin{description}
|
|
\item[Access Modifiers]
|
|
Classical OOP permits, generally, three common access modifiers: public,
|
|
protected and private. These access modifiers permit encapsulating data that
|
|
is unique \emph{per instance} of a given type, without the performance
|
|
penalties of privileged members (see \jsref{lst:privileged}).
|
|
|
|
Not only are access modifiers unsupported, but the concept of protected
|
|
members is difficult difficult in ECMAScript. In order for a member to be
|
|
accessible to other objects higher up on the prototype chain (``subtypes''),
|
|
they must be public. Using privileged members would encapsulate the data
|
|
within the constructor, forcing the use of public methods to access the data
|
|
and disallowing method overrides, effectively destroying any chances of a
|
|
protected API.\footnote{As ease.js will demonstrate, protected APIs are
|
|
possible through a clever hack that would otherwise lead to terrible,
|
|
unmaintainable code.}
|
|
\item[Intuitive Subtyping]
|
|
Consider the verbosity of \jsref{lst:subtype}. Now imagine how much
|
|
duplicate code is required to maintain many subtypes in a large piece of
|
|
software. This only serves to distract developers from the actual business
|
|
logic of the prototype, forcing them to think in detailed terms of
|
|
prototypes rather than in terms of the problem domain.\footnote{The ability
|
|
to think within the problem domain rather than abstract machine concepts is
|
|
one of the key benefits of classical object-oriented programming.}
|
|
|
|
Furthermore, as discussed in section~\ref{sec:ext-ctor}, creating extensible
|
|
constructors requires considerable thought that must be handled on a
|
|
case-by-case basis, or requires disproportionately complicated hacks (as
|
|
will be demonstrated in section~\ref{sec:extending}).
|
|
\end{description}
|
|
|
|
Fortunately,\footnote{Well, fortunately in the sense that ECMAScript is flexible
|
|
enough that we can work around the issues. It is, however, terribly messy. In
|
|
ECMAScript's defense --- this is a consequence of the prototypal model; our
|
|
desire to use class-like objects instead of conventional prototypes produces the
|
|
necessity for these hacks.} those issues can be worked around with clever hacks,
|
|
allowing us to continue closer toward a classical development model.
|