Inheritance in JavaScript is different than that in Java. While both
languages exploit inheritance to achieve code reuse and polymorphism,
they do so using different approaches.
Java uses
interface and
class types to exploit
inheritance. Although JavaScript has added classes to its syntax, they
are only shorthand for
an essentially object model, and, since JavaScript is loosely typed, it
has nothing like the concept of a Java interface.
Instead, JavaScript exploits an
object prototype model to achieve
inheritance through a
delegation design pattern.
In a class-based object-oriented language like Java, there are two
types of inheritance: interface inheritance and class (implementation)
inheritance.
An example of interface inheritance from our Java Problem Solver is
shown on the left below, while an example of class inheritance is shown
on the right. Advantages:
- Interface-typed variables can range over a wider variety of object
types, promoting polymorphism.
- Subclasses inherit public methods from
their superclasses, promoting code reuse.
JavaScript does not have true classes or interfaces, but it has a
mechanism built into its object model that allows objects to inherit
behavior from associated objects called
prototypes.
Every JavaScript object has a
prototype property that links to
another object, as in a chain. If two disparate objects have a need for
some behavior in common, they can each inherit that behavior from the
same prototype, eliminating the need for redundant code.
In the object diagram below, there are no classes or
interfaces. Instead, the unidirectional associations indicate object
relationships in which the work of some objects is
delegated by
disparate objects to common prototypes:
- On the left, the disparate state objects share the same prototype
object. This works because, as we shall see, the State
prototype is used only for its behavior.
- On the right the disparate mover objects each have their own copy
of the Mover prototype, because the move names and
functions specific to a domain are stored there. So while they
will share behavior, they must have their own storage.
We will use prototype inheritance to implement a problem solver
framework in JavaScript.
In this section we will describe the non-GUI
part of the framework, corresponding to that part of the Java problem
solver shown below.
State objects represent states of affairs in diverse domains, including
the farmer-wolf-goat-cabbage and 8-puzzle domains.
One feature all state objects have is the ability to display themselves
through their
toString methods.
In our introduction to the DOM, we used the HTML
textarea
element to test the display of
FarmerState objects.
In this section, we will define a
State object prototype that
will use the HTML
canvas element to flexibly display any state
object that has a
toString method defined for it.
The
canvas element provides a full-featured graphics environment
that is enabled through a JavaScript API. We will describe its full
capabilities later. For now, we will only use its ability to render
text.
The
State prototype has two methods relating to state display:
- makeDefaultCanvas: takes a state object as argument and
uses its toString method to return an HTML element suitable
for display. If a state object does not have graphical rendering
capability, it can call this method for a default state
display. It works by:
- Creating an HTML canvas element,
- Splitting the state's toString output into separate
text lines,
- Giving the canvas a width and height
depending on the text lines, and
- Drawing the text lines on the canvas.
- animateMove: takes a state object as argument and does
nothing. If a state object has graphical rendering capability that
includes animation, it will provide this method. We will see
examples later when we consider JavaScript graphics.
The constructor function for the
State object prototype can be
seen on the menu at left under
State.js.
Note that the prototype
object is created at the end of the file as
STATE_PROTO.
Note the use of the arrow syntax "
=>" in place of an
anonymous function in the loop. This is similar to a
lambda
expression.
Domain state constructor functions like
FarmerState
and
PuzzleState can make use of
STATE_PROTO to create
default state displays.
This section shows how the
FarmerState function presented
earlier can delegate work to the prototype.
We will see how states from another problem, the
Water Jug
Problem, can also benefit.
Implementing the 8-puzzle problem is left to a lab exercise.
We test the use of the prototype
STATE_PROTO in the HTML file
shown below.
The test of
FarmerState is similar to before, but it uses the
prototype to produce a more pleasing display.
The files involved can be seen from the menu on the left. Note:
- The new State.js file is loaded first
- The files are arranged in a directory structure that mirrors that
in our Java problem solver
FarmerStateTest.html
The
FarmerState constructor function has two additions to the
previous version (both at the end of the file):
- It sets its prototype property to STATE_PROTO
- It defines a makeCanvas method that delegates the canvas
rendering to the prototype
The test utilities file changes the
displayState function so
that it uses an object's
makeCanvas method to obtain a DOM
element to display.
The farmer state test file is the same as before. The output is shown
to the right.
Here is the
Water Jug Problem:
Below are string representations of three states of this problem, from
left to right:
- The initial state
- A state in which jug X has been filled (so it has 3 gallons)
- A state in which jug X has 2 gallons and jug Y has 4 gallons
(one of the final states)
| | | | |***|
| | | | |***| | | | | |***|
| | | | |***| | | |***| |***|
| | | | |***| | | |***| |***|
+---+ +---+ +---+ +---+ +---+ +---+
X Y X Y X Y
The files for testing water jug state representation are shown in the
menu at left.
Testing water jug states is similar to testing farmer states. The HTML
is shown below.
The
WaterJugState constructor function has the same structure
as
FarmerState:
- It stores object information (that is, the number of gallons in
X and Y)
- It defines the equals and toString methods
appropriate for its domain (although there is no safe method)
- It sets its prototype property to STATE_PROTO
- It defines a makeCanvas method that delegates the canvas
rendering to the prototype
The test file creates and tests various water jug state objects for
their display and equality.
The output is shown on the right.
In the Java Problem Solver framework,
Mover was an abstract
class type providing an efficient way to store and look up move
functions based on move names.
Move functions were expressed as
lambda expressions, and they were
efficiently accessed using Java
HashMaps.
JavaScript does not have abstract classes or HashMaps, but we can get
the same job done using objects and their prototypes.
Java HashMaps are implementations of the general concept of a
map,
which associates keys with values. The basic operations are:
- Create a new, empty map
- Put a new value onto the map given its key
- Get the value associated with a given key from the map
In hashed implementations of maps, the
put and
get
operations are constant-time.
JavaScript does not have a HashMap type, but JavaScript objects are
implemented so that they can efficiently and dynamically have
properties created and accessed. So JavaScript properties and their
values can fill the role of keys and values in maps.
Here is a JavaScript constructor function
Map that provides the
operations desired:
A
Mover prototype object is created using the constructor
function below. Note that the property and method names mirror those of
our Java
Mover class.
Since a
Mover prototype object is used for its state (storing
move names and move functions) as well as its behavior (
addMove,
doMove), each problem domain will need its own prototype copy.
This section shows how
domain mover constructor functions like FarmerMover
and WaterJugMover can make use of Mover prototypes to
store and retrieve move functions.
In this section we test the use of a
Mover prototype in the FWGC
problem.
The new files involved can be seen from the menu on the left.
The HTML test file is shown below.
The
FarmerMover constructor function defined in the file below
is empty; all of its work
is done by its
Mover prototype
fMover.
When a new instance of
FarmerMover is created, it will have
access to the move names and functions stored in
fMover.
Note that when moves are added to
fMover's map, JavaScript's
"arrow function" syntax is used, for example:
(s) => goAlone(s)
This is nearly identical to Java's lambda expression syntax.
The farmer mover test file first tests that valid move names are used,
then creates some states and tests the behavior
of
doMove on them.
The output is shown on the right.
In this section we test the use of a
Mover prototype in the
water jug problem.
The new files involved can be seen from the menu on the left.
The HTML test file is shown below.
The
WaterJugMover constructor function defined in the file below
is empty; all of its work
is done by its
Mover prototype
wjMover.
When a new instance of
WaterJugMover is created, it will have
access to the move names and functions stored in
wjMover.
The water jug mover test file first tests that valid move names are used,
then creates some states and tests the behavior
of
doMove on them.
The output is shown on the right.
In the Java Problem Solver framework,
Problem is a class that:
- Stores the problem name and introductory description
- Maintains the problem's current state, initial state, and final
state
- Contains an instance of the appropriate Mover object for
the problem
- Defines a default success method that can be
overridden by subclasses
We will accomplish the same thing with JavaScript problem object
prototypes.
A
Problem prototype object is created using the constructor
function below. Note that the property and method names mirror those of
our Java
Problem class.
Since a
Problem prototype object is used for its state
(e.g. current state, mover object, etc.) as well as its behavior
(
success), each problem domain will need its own prototype
copy.
This section shows how
domain problem constructor functions like FarmerProblem
and WaterJugProblem make use of Problem prototypes.
Here we test the use of a
Problem prototype in the FWGC
problem.
The files involved can be seen from the menu on the left.
The HTML test file is shown below.
The
FarmerProblem constructor function defined in the file below
is empty; all of its work
is done by its
Problem prototype.
When a new instance of
FarmerProblem is created, it will have
access to the properties and methods stored in the prototype.
The farmer problem test file first displays the problem name and introduction,
then sets the problem's current state and tests the
success method.
The output is shown on the right.
Here we test the use of a
Problem prototype in the water jug
problem.
The files involved can be seen from the menu on the left.
The HTML test file is shown below.
The water jug problem is different than other problems in that there is
no unique goal state; rather there is a
goal condition, which is 2
gallons in either jug.
The
WaterJugProblem constructor function below
redefines the
success method from its
Problem prototype
to reflect this condition.
When a new instance of
WaterJugProblem is created, it will have
access to the properties and methods stored in the prototype.
The water jug problem test file first displays the problem name and introduction,
then sets the problem's current state and tests the
success method.
The output is shown on the right.
Recall that in the Java Problem Solver, the
SolvingAssistant
class is responsible for:
- Trying and counting moves
- Updating the current state after a valid move
- Checking whether the problem is solved
- Resetting the problem
We create a
SolvingAssistant object (not a prototype) that
carries out these responsibilities.
We test it on a
FarmerProblem using the HTML file below. The
JavaScript files are shown on the menu at left.
The test file tries a few invalid moves then solves the problem. It is
shown below with the output to the right.