facebook google plus twitter
Webucator's Free Advanced JavaScript Programming Tutorial

Lesson: Advanced Objects

Welcome to our free Advanced JavaScript Programming tutorial. This tutorial is based on Webucator's Advanced JavaScript Programming course.

Prototyping in JavaScript allows us to use object-oriented concepts such as objects, properties and methods, and other features. The recent ES2015 release of JavaScript introduces a new class syntax to prototyping. Advanced objects like Map and Set are useful data structures.

Lesson Goals

  • Use prototyping in JavaScript.
  • Write classes using the new ES2015 syntax.
  • Write static methods.
  • Write subclasses - to use inheritance.
  • Use the Map and Set objects.
  • Use ES2015 modules.

Object-Oriented Programming

Object-oriented programming, often abbreviated as "OOP", is a coding paradigm centered on the idea of an "object". Objects have properties or fields relevant to themselves: a "person" object, for instance, might have attributes for "name", "address", "occupation", etc.

Objects also have methods: these are functions that can get or set the values of the object's attributes and offer a mechanism for messaging between objects and other code. A "bank account" object, for instance, might have a "balance" attribute (storing the amount of money currently in the account) and a "doDeposit" method, which accepts an "amount" parameter and increments the balance attribute accordingly.

Prototyping

Objects in JavaScript

You have already seen objects in JavaScript. In simple terms, objects are associative arrays or hashes: arrays where the element index is not a number starting at 0, but rather a string. Object elements can be methods (functions) as well: a function that returns a value. Consider the following example:

Code Sample:

AdvancedObjects/Demos/objects.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Simple Objects in JavaScript</title>
</head>
<body>
	<h1>Simple Objects in JavaScript</h1>
	<script>
		var jdoe = {fname:'Jane', lname:'Doe', age:32, fullName:function() {return this.fname + ' ' + this.lname}}
		document.write('First name: ');
		document.write(jdoe.fname);
		document.write('<hr>');
		document.write('Full name: ');
		document.write(jdoe.fullName());
	</script>
</body>
</html>

Code Explanation

On line 10 we create an object jdoe, with properties fname, lname, and age. The object also has method fullName() which returns the full name of the person by concatenating the first and last names together with a space. We access (as on lines 12 and 15) the properties and methods of an object with the dot operator: jdoe.fname, jdoe.fullName(), etc.

Prototyping in JavaScript

Our previous example gets us some way toward writing OOP code in JavaScript, but we need more. While jdoe is a perfectly nice, useful object, it would be helpful if we could define a pattern for any person, not just for one particular person named "Jane Doe". That way, if we decide later to add a field ("address", say, or "telephone number") for our person object, we wouldn't have to do it for every object.

In JavaScript, we use prototyping for this purpose: we create an object, assigning to it the required fields (attributes and methods), and then instantiate (create) other objects from it using the new keyword. The prototype object serves as a pattern or template for the other objects, all of which have the same fields as defined in the prototype. Let's look at an example:

Code Sample:

AdvancedObjects/Demos/prototyping.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Prototyping</title>
	<script>
		var Person = function(fname, lname, age) {
			this.fname = fname;
			this.lname = lname;
			this.age = age;
			this.fullName = function() {
				return this.fname + ' ' + this.lname;
			}
		}
	</script>
</head>
<body>
	<h1>Prototyping</h1>
	<script>
		var jdoe = new Person('Jane', 'Doe', 32);
		document.write('First name: ');
		document.write(jdoe.fname);
		document.write('<hr>');
		document.write('Full name: ');
		document.write(jdoe.fullName());

		document.write('<hr>');
		document.write('<hr>');

		var afung = new Person('Adam', 'Fung', 11);
		document.write('First name: ');
		document.write(afung.fname);
		document.write('<hr>');
		document.write('Full name: ');
		document.write(afung.fullName());
	</script>
</body>
</html>

Code Explanation

On line 7 we create an object Person - this object, with attributes (first and last names, age) and method (fullName()) matching our earlier example - serves as the prototype for the objects (jdoe, afung) we create below. Note the we create each of the prototyped objects with the new keyword.

Every JavaScript object has a prototype from which it inherits its fields - its methods and properties; the top-most ancestor of any object is the generic JavaScript Object object. You may be familiar with some built-in JavaScript objects - like Date, String, or Math. These built-in objects (which offer useful properties and methods for dealing with date, text-string, or mathematical entities) inherit, like any object, from Object.

Even when we used JavaScript objects previously just as associative arrays - when we used something like this:

var person = {fname:'Jane', lname:'Doe', age:32}

our object in fact inherited from Object. But now we are explicitly exploiting this inhertance to use objects for prototyping, to create an object-oriented class from which to create other objects.

We can "tack on" new methods to our existing prototype by using the prototype keyword, even after having defined the prototype object. Consider the following example:

Code Sample:

AdvancedObjects/Demos/addingPrototypeMethods.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Prototyping: Adding Methods</title>
	<script>
		var Person = function(fname, lname, age) {
			this.fname = fname;
			this.lname = lname;
			this.age = age;
			this.fullName = function() {
				return this.fname + ' ' + this.lname;
			}
		}
	</script>
</head>
<body>
	<h1>Prototyping: Adding Methods</h1>
	<script>
		var jdoe = new Person('Jane', 'Doe', 32);
		
		Person.prototype.isOldEnoughToVote = function() {
			return this.age >= 18;
		}

		Person.prototype.changeAge = function(age) {
			//test if age is an integer:
			if (!(Number(age) === age && age % 1 === 0)) {
				return;
			}
			//test if age is >= 0
			if (age <= 0) {
				return;
			}
			this.age = age;
		}

		document.write('First name: ');
		document.write(jdoe.fname);
		document.write('<hr>');
		document.write('Full name: ');
		document.write(jdoe.fullName());
		document.write('<hr>');
		document.write('Old enough to vote? ');
		document.write(jdoe.isOldEnoughToVote() ? 'yes' : 'no');

		jdoe.changeAge(4);

		document.write('<hr>');
		document.write('Age: ');
		document.write(jdoe.age);
		document.write('<hr>');
		document.write('Old enough to vote? ');
		document.write(jdoe.isOldEnoughToVote() ? 'yes' : 'no');

		jdoe.changeAge('stringnotanumber');

		document.write('<hr>');
		document.write('Age: ');
		document.write(jdoe.age);
		document.write('<hr>');
		document.write('Old enough to vote? ');
		document.write(jdoe.isOldEnoughToVote() ? 'yes' : 'no');
	</script>
</body>
</html>

Code Explanation

We define our Person object prototype as before. On line 20, we create a new object (jdoe) of prototype Person. Next, we add two new methods to the prototype:

  1. Function isOldEnoughToVote, which tests whether the object's age would allow them to vote in the United States, returning true if the age is greater than or equal to 18.
  2. Function changeAge, which checks to make sure that the supplied parameter is an integer and is greater than or equal to zero. If either of these conditions is not true (if the age parameter is a string, say, or has a value like -2) then the function returns without modifying the prototype member age. If the conditions are satisfied - if we have a valid age - then this.age is set to age.

Note that, even though we instantiated object jdoe (on line 20) before adding these prototype methods (beginning on line 22), we can call these methods on jdoe later.

Composing Prototypes

As your JavaScript code grows more complex, you'll often find it helpful to combine prototypes: member data in one prototype may consist of objects from another prototype. Consider the following example, in which we create a "Team" prototype (to represent a sports team); a member of each team is an array of people who are players on that team. Each element of the players array is itself an object of the Person prototype:

Code Sample:

AdvancedObjects/Demos/multipleobjects.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Multiple Prototypes</title>
	<script>
		var Person = function(fname, lname, age) {
			this.fname = fname;
			this.lname = lname;
			this.age = age;
			this.fullName = function() {
				return this.fname + ' ' + this.lname;
			}
		}

		var Team = function(teamName, sport) {
			this.teamName = teamName;
			this.sport = sport;
			this.players = [];
			            this.addPlayer = function(person) {
				this.players.push(person);
			}
			            this.toString = function() {
				var str = '<strong>' + this.teamName + '</strong>';
				str += '<ul>';
				for (let p in this.players) {
					if (this.players.hasOwnProperty(p)) {
						let person = this.players[p];
						str += '<li>' + person.fname + ' ' + person.lname + '</li>';
					}
				}
				str += '</ul>';
				return str;
			}
		}
	</script>
</head>
<body>
	<h1>Multiple Prototypes</h1>
	<script>
		var jdoe = new Person('Jane', 'Doe', 32);
		var afung = new Person('Adam', 'Fung', 11);
		
		var Tigers = new Team('The Tigers', 'soccer');
		Tigers.addPlayer(jdoe);
		Tigers.addPlayer(afung);

		document.write('Here is my team:<br><br>');
		document.write(Tigers);
	</script>
</body>
</html>

Code Explanation

Note that we are using ES2015 code here. We again define a Person prototype. We also define a Team prototype, with members for the name of the team and the type of sport. Each Team object also has access to two methods:

  1. Function addPlayer(person) adds a Person object to the this.players array.
  2. Function toString() returns a string-ified version of the Team object: the bolded name of the team and an unordered list of the team's players, which we generate by iterating over the array of team members.

The toString() method deserves special mention: toString() is a built-in JavaScript function - a method of the Object prototype. By overriding it - that is, by writing a custom version of toString() that is specific to and appropriate for our particular Team prototype - we tell JavaScript how it is that we would like any object of Team to be turned into a string. This allows us to use the shorthand document.write(Tigers) to write an object of Team to the screen or to turn it into a string for any other purpose.

We'll ask you to try out these concepts in the next exercise:

Prototyping

Duration: 20 to 30 minutes.

In this exercise, you will create two prototypes to represent vehicles and a company's fleet of vehicles.

  1. Open AdvancedObjects/Exercises/vehicles.html for editing.
  2. Create a prototype for Vehicle representing a vehicle with a given type ('car', 'bus', etc.), year purchased, and VIN (vehicle identification number); the fields and methods are listed in the comment.
  3. Create a prototype for Fleet representing a company's collection of vehicles; the fields and methods are listed in the comment.
  4. The HTML markup for the page includes a form, a div to display the current fleet, and a button which the user can click to get the current average age of the fleet vehicles; the HTML markup and JavaScript event handling code is already done for you.
  5. When the page loads, a window.onload event handler instantiates a new Fleet object; where indicated in the comments, add JavaScript code to add a vehicle, to display the current fleet, and to display the average age of fleet vehicles in response to user actions.
  6. Test your solution in a browser.

Solution:

AdvancedObjects/Solutions/vehicles.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Company Fleet</title>
	<style>
		form#addVehicle {
			float:left;
			width:48%;
		}
		div#info {
			float:right;
			width:48%;
		}
	</style>
	<script>
		var Vehicle = function(vehicleType, yearPurchased, VIN) {
			this.vehicleType = vehicleType;
			this.yearPurchased = yearPurchased;
			this.VIN = VIN; //vehicle identification number
			this.toString = function() {
				return this.vehicleType + ', purchased ' + this.yearPurchased + ', VIN: ' + this.VIN;
			}
		}

		var Fleet = function(companyName) {
			this.companyName = companyName;
			this.vehicles = [];
			this.addVehicle = function(vehicle) {
				this.vehicles.push(vehicle);
			}
			this.toString = function() {
				var str = 'Current fleet for <strong>' + this.companyName + '</strong>:';
				str += '<ul>';
				for (let v in this.vehicles) {
					if (this.vehicles.hasOwnProperty(v)) {
						let vehicle = this.vehicles[v];
						str += '<li>' + vehicle + '</li>';
					}
				}
				str += '</ul>';
				return str;
			}
			this.averageAgeOfVehicles = function() {
				if (this.vehicles.length === 0) {
					return 0;
				}
				var yearSum = 0;
				for (let v in this.vehicles) {
					if (this.vehicles.hasOwnProperty(v)) {
						let vehicle = this.vehicles[v];
						yearSum += parseInt(vehicle.yearPurchased);
					}
				}
				var averageYear = parseInt(yearSum / this.vehicles.length);
				var curYear = new Date().getFullYear();
				return curYear - averageYear;
			}
		}

		function clearFields() {
			document.getElementById('vehicleType').value = '';
			document.getElementById('yearPurchased').value = '';
			document.getElementById('VIN').value = '';
		}

		window.onload = function() {
			var companyFleet = new Fleet('ABC, Inc.');
			document.getElementById('currentFleetInfo').innerHTML = companyFleet;

			var addVehicleForm = document.getElementById('addVehicle');
			addVehicleForm.addEventListener('submit',
				function(event) {
					event.preventDefault();
					var vehicleType = document.getElementById('vehicleType').value;
					var yearPurchased = document.getElementById('yearPurchased').value;
					var VIN = document.getElementById('VIN').value;
					companyFleet.addVehicle(new Vehicle(vehicleType, yearPurchased, VIN));
					document.getElementById('currentFleetInfo').innerHTML = companyFleet;
					clearFields();
				},
				false);
			var getAverageAgeButton = document.getElementById('getAverageAge');
			getAverageAgeButton.addEventListener('click',
				function() {
					var averageAge = companyFleet.averageAgeOfVehicles();
					alert('The average age of all vehicles is ' + averageAge + ' years');
				},
				false);
		}
	</script>
</head>
<body>
	<h1>Company Fleet</h1>
	<form id="addVehicle">
		<select id="vehicleType">
			<option value="">-vehicle type-</option>
			<option value="car">car</option>
			<option value="van">van</option>
			<option value="truck">truck</option>
			<option value="bus">bus</option>
		</select>
		<input type="text" id="yearPurchased" placeholder="year purchased">
		<input type="text" id="VIN" placeholder="VIN">
		<input type="submit" value="Add Vehicle">
	</form>
	<div id="info">
		<div id="currentFleetInfo"></div>
		<button id="getAverageAge">Average Age of Vehicles</button>
	</div>
</body>
</html>

Code Explanation

Note that we are using ES2015 code here; specifically, the let keyword. We create prototypes for Vehicle and Fleet, each with the specified data members. Both prototypes have a toString() method, dictating how each should be turned into a string; note that, in the toString() method for Fleet we use the toString() method from Vehicle:

str += '<li>' + vehicle + '</li>'

For the toString() method in Fleet, we iterate over the array elements of this.vehicles to build an ordered list of all vehicles.

For method averageAgeOfVehicles() we check first to see if there are no vehicles in the array (that is, if this.vehicles.length === 0), returning 0 if so; if there is at least one vehicle in the array, then we iterate over the array to build a running sum of the year each vehicle was purchased. We find an average of these years, convert it to an integer, and subtract that value from the current year.

When the window.onload event fires, we set the inner HTML of the div with id currentFleetInfo to display the current fleet - which, on initial page load, will have no vehicles.

When the user adds a vehicle, we invoke the Fleet method addVehicle() to add a vehicle from the information entered by the user. Note that we do not need to create a named Vehicle object: we create a new Vehicle "on the fly" and supply it as the parameter:

CompanyFleet.addVehicle(new Vehicle(vehicleType, yearPurchased, VIN));

Lastly, we invoke method CompanyFleet.averageAgeOfVehicles() in response to the user clicking the #getAverageAge button and generate an alert popup message.

ES2015 Classes

The ECMAScript 6 (often referred to as "ES2015") release of JavaScript introduced a new syntax for OOP in JavaScript. (Technically, the release is called "ECMAScript 2015" and this is the 6th edition; we will use the common term "ES2015" throughout this course.) While the underlying code remains the same - we are still using prototyping of the sort we looked at previously in this lesson - the introduction of the class keyword and some other features allow us to work with object-oriented code in a fashion similar to that used in other programming languages.

Please be aware that, because ES2015 is a relatively-new standard, ES2015 features (like class) may not work in older browsers.

At the core of the new syntax is the class and a constructor for that class; the constructor is the method invoked when we instantiate an object of the class with the new keyword - just like the function we defined to serve as the prototype for objects previously. Here's an example with the new syntax to match our earlier example:

Code Sample:

AdvancedObjects/Demos/class.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Class</title>
	<script>
		class Person {
			constructor(fname, lname, age) {
				this.fname = fname;
				this.lname = lname;
				this.age = age;
			}
			fullName() {
				return this.fname + ' ' + this.lname;
			}
		}
	</script>
</head>
<body>
	<h1>Class</h1>
	<script>
		var jdoe = new Person('Jane', 'Doe', 32);
		document.write('First name: ');
		document.write(jdoe.fname);
		document.write('<hr>');
		document.write('Full name: ');
		document.write(jdoe.fullName());

		document.write('<hr>');
		document.write('<hr>');

		var afung = new Person('Adam', 'Fung', 11);
		document.write('First name: ');
		document.write(afung.fname);
		document.write('<hr>');
		document.write('Full name: ');
		document.write(afung.fullName());
	</script>
</body>
</html>

Code Explanation

Where before we created a function to serve as our prototype, we now do the same thing using the new ES2015 class syntax. The class defines the prototype for objects created with the new keyword (as we do on line 22), with the constructor - a special kind of method - specifying how the data members of the class get assigned. We also create, as we did before, a method fullName() - a function which returns the first and last names concatenated together with a space.

The new class syntax is, we would argue, cleaner and simpler and, for many of us, more familiar from OOP syntax we find in other language. Anything we could do with the prototyping syntax we looked at earlier in this lesson we can do with the new ES2015 syntax. Here's another example:

Code Sample:

AdvancedObjects/Demos/multipleObjectsClasses.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Multiple Classes</title>
	<script>
		class Person {
			constructor(fname, lname, age) {
				this.fname = fname;
				this.lname = lname;
				this.age = age;
			}
			toString() {
				return this.fname + ' ' + this.lname;
			}
		}

		class Team {
			constructor(teamName, sport) {
				this.teamName = teamName;
				this.sport = sport;
				this.players = [];
			}
			addPlayer(person) {
				if (person.age >= 18) {
					this.players.push(person);
					return true;
				}
				return false;
			}
			toString() {
				var str = '<strong>' + this.teamName + '</strong>';
				str += '<ul>';
				for (let p in this.players) {
					let person = this.players[p];
					str += '<li>' + person + '</li>';
				}
				str += '</ul>';
				return str;
			}
		}
	</script>
</head>
<body>
	<h1>Multiple Classes</h1>
	<script>
		var jdoe = new Person('Jane', 'Doe', 32);
		var afung = new Person('Adam', 'Fung', 11);
		
		var tigers = new Team('The Tigers', 'soccer');
		if (!tigers.addPlayer(jdoe)) {
			alert('player not added - too young');
		}
		if (!tigers.addPlayer(afung)) {
			alert('player ' + afung + ' not added - too young');
		}

		document.write('Here is my team:<br><br>');
		document.write(tigers);
	</script>
</body>
</html>

Code Explanation

We create classes for Person (replacing the fullName() method with toString()) and Team. We modify the Team class slightly from our earlier example so that only people who are 18 years old or older can be added to this team. Method addPlayer checks the age of its person argument, adding the person to the players array and returning true if the person is old enough; the method returns false (and does not add the person) if too young. We check (on lines 51 and 54) to see if the person was added when calling Tigers.addPlayer, alerting a message to the user if not.

Static Methods

So far we have looked at instance methods for our classes/prototypes: functions which act upon the specific data associated with an object of the class. We create a method, like toString(), which acts upon or gets information from the property values of each object. If we had two Person objects jdoe and afung, then invoking the toString() method on object jdoe might return "Jane Doe", while invoking toString() on afung might return "Adam Fung".

But sometimes we want a method which belongs to the class as a whole, rather than to an instance of the class. In JavaScript, like in many other programming languages, we call this a static method, using the static keyword when defining the method. We'll look now at an example where we model an ordered pair - a point on a graph with (x,y) coordinates - as a way of demonstrating.

Code Sample:

AdvancedObjects/Demos/point.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Class</title>
	<script>
		class Point {
			constructor(x, y) {
				this.x = x;
				this.y = y;
			}
			toString() {
				return '(' + this.x + ',' + this.y + ')';
			}
			static distance(p1, p2) {
				var xDif = p1.x - p2.x;
				var yDif = p1.y - p2.y;
				return Math.sqrt(xDif*xDif + yDif*yDif);
			}
		}
	</script>
</head>
<body>
	<h1>Class</h1>
	<script>
		var point1 = new Point(3, 4);
		var point2 = new Point(-1, 6);
		document.write('Distance between ' + point1 + ' and ' + point2 + ' is ' + Point.distance(point1, point2));
	</script>
</body>
</html>

Code Explanation

Our Point class models a point on a graph, with data members x and y to represent a point like (3,4), a point with x-coordinate 3 and y-coordinate 4, or (-1,6).

The toString() method returns a string representation of the Point, with opening parenthesis, x coordinate, comma, y coordinate, and closing parenthesis.

We also define a static method distance. This method belongs to the class as a whole: it wouldn't make sense to ask "what is the distance" for any one Point object, since distance by its very nature is the distance between two points. Thus this method is defined with the static keyword. We call the method, on line 28, using not an object of the class but rather with the class name: that is, we use Point.distance(point1, point2) rather than something like point1.distance(point1, point2).

Keep in mind that, because static methods are associated with the class rather than an instance (object) of the class, we can't access data members in the definition of the static method. It wouldn't make sense, in the body of a static method definition, to refer to this.x or this.fname, since a static method isn't acting on (doesn't know anything about) a specific object.

Let's look at another example - adding a static method to our Person class:

Code Sample:

AdvancedObjects/Demos/staticMethodPerson.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Class</title>
	<script>
		class Person {
			constructor(fname, lname, age) {
				this.fname = fname;
				this.lname = lname;
				this.age = age;
			}
			toString() {
				return this.fname + ' ' + this.lname;
			}
			static possibleParentChild(p1, p2) {
				if (p1.lname !== p2.lname) {
					return false;
				}
				if (Math.abs(p1.age - p2.age) < 18) {
					return false;
				}
				return true;
			}
		}
	</script>
</head>
<body>
	<h1>Class</h1>
	<script>
		var jdoe = new Person('Jane', 'Doe', 32);
		var afung = new Person('Adam', 'Fung', 11);
		var mdoe = new Person('Maria', 'Doe', 13);
		
		if (Person.possibleParentChild(jdoe, mdoe)) {
			document.write(jdoe + ' and ' + mdoe + ' might be parent/child');
		} else {
			document.write(jdoe + ' and ' + mdoe + ' are likely not parent/child');
		}
	</script>
</body>
</html>

Code Explanation

Consider the case of gauging the probability that two Person objects represent a parent and child. While certainly not definitive, we might say it is true that two such objects are likely to be a parent and child is they have the same last name and if their ages are at least some threshold value apart.

We define static method possibleParentChild for class Person which expects two Person objects as parameters and returns true if their lname fields are equal and if their ages differ by at least 18.

We call the static method on line 35 with the code Person.possibleParentChild(jdoe, mdoe) - again, using the name of the class (Person) to invoke a static method rather than using an object of the class.

We'll ask you to try out using ES2015 classes and static methods in the next exercise:

ES2015 Classes & Static Methods

Duration: 15 to 25 minutes.

In this exercise, you will rewrite our Vehicle/Fleet code using ES2015 classes, and add a static method.

  1. Open AdvancedObjects/Exercises/vehiclesclasses.html for editing.
  2. Where indicated by the comments, complete class Vehicle:
    • Add a constructor
    • Add a toString() method
    • Write static method Compare(v1, v2) to compare two Vehicles, returning 1, -1, or 0, as defined in the comments
  3. Where indicated by the comments, complete class Fleet:
    • Add a constructor
    • Add method addVehicle(vehicle)
    • Add method toString()
    • Method averageAgeOfVehicles() is done for you
  4. The HTML markup and event-handling code is done for you. The page now includes a form, presented to users when there are at least two vehicles in the fleet, to be used to compare any two vehicles. Write code to invoke method Compare(v1, v2) and send the result in an message.
  5. Test your solution in a browser.

Solution:

AdvancedObjects/Solutions/vehiclesclasses.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Company Fleet</title>
	<style>
		form#addVehicle {
			float:left;
			width:48%;
		}
		div#info {
			float:right;
			width:48%;
		}
		form#compare {
			margin-top:15px;
		}
	</style>
	<script>
		class Vehicle {
			constructor(vehicleType, yearPurchased, VIN, purchasePrice) {
				this.vehicleType = vehicleType;
				this.yearPurchased = yearPurchased;
				this.VIN = VIN; //vehicle identification number
				this.purchasePrice = purchasePrice.replace(/\$/g,'');
			}
			toString() {
				return this.yearPurchased + ' ' + this.vehicleType + ', $' + parseInt(this.purchasePrice).formatMoney(0) + ' VIN: ' + this.VIN;
			}
			static Compare(v1, v2) {
				if (v1.purchasePrice !== v2.purchasePrice) {
					if (v1 < v2) {
						return 1;
					} else {
						return -1;
					}
				} else if (v1.yearPurchased !== v2.yearPurchased) {
					if (v1.yearPurchased > v2.yearPurchased) {
						return 1;
					} else {
						return -1;
					}
				}
				return 0;
			}
		}

		class Fleet {
			constructor(companyName) {
				this.companyName = companyName;
				this.vehicles = [];
			}
			addVehicle(vehicle) {
				this.vehicles.push(vehicle);
			}
			toString() {
				var str = 'Current fleet for <strong>' + this.companyName + '</strong>:';
				str += '<ul>';
				for (let v in this.vehicles) {
					if (this.vehicles.hasOwnProperty(v)) {
						let vehicle = this.vehicles[v];
						str += '<li><strong>' + v + '</strong>: ' + vehicle + '</li>';
					}
				}
				str += '</ul>';
				return str;
			}
			averageAgeOfVehicles() {
				if (this.vehicles.length === 0) {
					return 0;
				}
				var yearSum = 0;
				for (let v in this.vehicles) {
					if (this.vehicles.hasOwnProperty(v)) {
						let vehicle = this.vehicles[v];
						yearSum += parseInt(vehicle.yearPurchased);
					}
				}
				var averageYear = parseInt(yearSum / this.vehicles.length);
				var curYear = new Date().getFullYear();
				return curYear - averageYear;
			}
		}

		Number.prototype.formatMoney = function(c, d, t) {
			var n = this, 
				c = isNaN(c = Math.abs(c)) ? 2 : c, 
				d = d == undefined ? "." : d, 
				t = t == undefined ? "," : t, 
				s = n < 0 ? "-" : "", 
				i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", 
				j = (j = i.length) > 3 ? j % 3 : 0;
					return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
		 };

		function clearFields() {
			document.getElementById('vehicleType').value = '';
			document.getElementById('yearPurchased').value = '';
			document.getElementById('VIN').value = '';
			document.getElementById('purchasePrice').value = '';
		}

		window.onload = function() {
			var CompanyFleet = new Fleet('ABC, Inc.');
			document.getElementById('currentFleetInfo').innerHTML = CompanyFleet;

			var addVehicleForm = document.getElementById('addVehicle');
			addVehicleForm.addEventListener('submit',
				function(eventevent) {
					event.preventDefault();
					var vehicleType = document.getElementById('vehicleType').value;
					var yearPurchased = document.getElementById('yearPurchased').value;
					var VIN = document.getElementById('VIN').value;
					var purchasePrice = document.getElementById('purchasePrice').value;
					CompanyFleet.addVehicle(new Vehicle(vehicleType, yearPurchased, VIN, purchasePrice));
					document.getElementById('currentFleetInfo').innerHTML = CompanyFleet;
					clearFields();
					if (CompanyFleet.vehicles.length >= 2) {
						var select1 = '';
					    select1 += '<select id="v1">';
					    for (var v=0; v<CompanyFleet.vehicles.length; v++) {
							select1 += '<option value="'+v+'">Vehicle '+v+'</option>';
						}
					    select1 += '</select>';

						var select2 = '';
					    select2 += '<select id="v2">';
					    for (var v=0; v<CompanyFleet.vehicles.length; v++) {
							select2 += '<option value="'+v+'">Vehicle '+v+'</option>';
						}
					    select2 += '</select>';

					    var submitButton = '<input type="submit" value="Compare">';

					    var compareForm = document.getElementById('compare');
					    compareForm.innerHTML = select1 + ' ' + select2 + ' ' + submitButton;
					}
				},
				false);
			var getAverageAgeButton = document.getElementById('getAverageAge');
			
			getAverageAgeButton.addEventListener('click',
				function() {
					var averageAge = CompanyFleet.averageAgeOfVehicles();
					alert('The average age of all vehicles is ' + averageAge + ' years');
				},
				false);

			var compareForm = document.getElementById('compare');
			compareForm.addEventListener('submit',
				function(event) {
					event.preventDefault();
					var selectedVehicle1 = document.getElementById('v1').value;
					var selectedVehicle2 = document.getElementById('v2').value;
					var compareResult = Vehicle.Compare(CompanyFleet.vehicles[selectedVehicle1], CompanyFleet.vehicles[selectedVehicle2]);
					var msg = '';
					if (compareResult > 0) {
						msg = 'Vehicle ' + selectedVehicle1 + ' is better';
					} else if (compareResult < 0) {
						msg = 'Vehicle ' + selectedVehicle2 + ' is better';
					} else {
						msg = 'Vehicles are the same';
					}
					alert(msg);
				},
				false);
		}
	</script>
</head>
<body>
	<h1>Company Fleet</h1>
	<form id="addVehicle">
		<select id="vehicleType">
			<option value="">-vehicle type-</option>
			<option value="car">car</option>
			<option value="van">van</option>
			<option value="truck">truck</option>
			<option value="bus">bus</option>
		</select>
		<input type="text" id="yearPurchased" placeholder="year purchased">
		<br>
		<input type="text" id="VIN" placeholder="VIN">
		<input type="text" id="purchasePrice" placeholder="purchase price">
		<br>
		<input type="submit" value="Add Vehicle">
	</form>
	<div id="info">
		<div id="currentFleetInfo"></div>
		<button id="getAverageAge">Average Age of Vehicles</button>
		<form id="compare"></form>
	</div>
</body>
</html>

Code Explanation

The definitions for the classes are similar to the work we did in the previous exercise; we now use the new ES2015 class syntax.

We add static method Compare(v1, v2) to class Vehicle:

  • If the vehicles' purchase prices differ, we return 1 or -1 as appropriate;
  • Else if the vehicles' year of purchase differ, we return 1 or -1 as appropriate;
  • Otherwise, we return 0 (since the vehicles are, by this calculus, the same).

In the event handler for the user submitting the #compare form, we set variable compareResult = Vehicle.Compare(CompanyFleet.vehicles[selectedVehicle1], CompanyFleet.vehicles[selectedVehicle2]), comparing the two user-selected vehicles via the static method Compare, build a string message based on the results, and return the message to the user with an alert popup.

Inheritance

One of the real benefits of object-oriented programming is the ability to build upon existing classes - to inherit all of the functionality of a well-written class and extend it for some purpose specific to your needs.

Imagine that you have a Person class - a complex set of code that includes all of the functionality you need, that has been tested in production for years, but yet which is missing just a few things that you need for a new application. Everything is great - you just need to add a field for "EmployeeID" or something similar, but the rest of the class works fine. In JavaScript (like in many other languages) we would here create a subclass using the extends keyword; note that this keyword is new to ES2015. Let's look at a simple example to see how this works:

Code Sample:

AdvancedObjects/Demos/pets.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Pets</title>
	<script>
		class Pet {
			constructor(name, color) {
				this.name = name;
				this.color = color;
				this.animaltype = 'animal';
			}
			toString() {
				return this.name + ' - a ' + this.color + ' ' + this.animaltype;
			}
			speak() {
				return 'ehh';
			}
		}

		class Dog extends Pet {
			constructor(name, color) {
				super(name, color);
				this.animaltype = 'dog';
			}
			speak() {
				return 'woof';
			}
		}

		class Bird extends Pet {
			constructor(name, color, wingspan) {
				super(name, color);
				this.animaltype = 'bird';
				this.wingspan = wingspan;
			}
			toString() {
				return this.name + ' - a ' + this.color + ' ' + this.animaltype 
					+ ' with wingspan of ' + this.wingspan + ' centimeters';
			}
			speak() {
				return 'cheep';
			}
		}
	</script>
</head>
<body>
	<h1>Pets</h1>
	<script>
		var pet = new Pet('Cuddles', 'brown');
		document.write(pet);
		document.write('<br>');
		document.write(pet.name + ' says "' + pet.speak() + '"');
		document.write('<hr>');
		var fido = new Dog('Fido', 'brown');
		document.write(fido);
		document.write('<br>');
		document.write(fido.name + ' says "' + fido.speak() + '"');
		document.write('<hr>');
		var feathers = new Bird('Feathers', 'yellow', 7);
		document.write(feathers);
		document.write('<br>');
		document.write(feathers.name + ' says "' + feathers.speak() + '"');
	</script>
</body>
</html>

Code Explanation

We define class Pet with properties name (the name of the pet) and color - both set when the constructor is called - as well as animaltype, which we always set to the value 'animal'. Our Pet class includes a toString() method (returning something like "Cuddles - a brown animal") and a speak() method. All instances of class Pet speak "ehh".

The Pet class, while simple, works great. We can instantiate a Pet object, write it to the screen (implicitly invoking its toString() method), and ask it to speak().

But the generic Pet class won't serve our needs for more-specific types of pets: we might want to keep track of the wingspan of a pet which is a bird, but it wouldn't make sense to add a property to the Pet class for wingspan, since for most types of pets "wingspan" would be irrelevant.

Thus we turn to inheritance: we create two more-specific classes (Dog and Bird), each of which handles data and behavior appropriate for a particular kind of pet. Note that both use the keyword extends in their definition, to indicate that each class is a subclass of (inherits from) Pet.

The Dog class constructor invokes the parent-class (Pet) constructor by calling super(name, color) on line 23. Since any Dog object is also a Pet object, calling super(name, color) has the effect of setting this.name = name and this.color = color. Dog objects inherit the properties and methods of their parent, so any Dog has both a name and a color. In the Dog constructor we set this.animaltype to 'dog'.

We don't need to modify the toString() method for the Dog class; since we set any object of class Dog to have animaltype 'dog', the toString() method for Dog will return something like "Fido - a brown dog".

We override the default speak() method for the Dog class: where any Pet would say "ehh", any Dog would say "woof".

We do similar things in the Bird class - also a subclass of Pet. In addition, we add a property (wingspan) specific to birds, and override the toString() method to include the wingspan information - something like "Feathers - a yellow bird with wingspan of 7 centimeters".

Note that, when we instantiate an object (feathers) of class Bird on line 59, we include an extra parameter for the wingspan.

Let's review another example, in which we modify our Person class to include a team jersey number of members of our team.

Code Sample:

AdvancedObjects/Demos/multipleObjectsInheritance.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Multiple Classes with Inheritance</title>
	<script>
		class Person {
			constructor(fname, lname, age) {
				this.fname = fname;
				this.lname = lname;
				this.age = age;
			}
			toString() {
				return this.fname + ' ' + this.lname;
			}
		}

		class Player extends Person {
			constructor(fname, lname, age, jerseyNumber) {
				super(fname, lname, age);
				this.jerseyNumber = jerseyNumber;
			}
			toString() {
				return super.toString() + ' (#' + this.jerseyNumber + ')';
			}
		}

		class Team {
			constructor(teamName, sport) {
				this.teamName = teamName;
				this.sport = sport;
				this.players = [];
			}
			addPlayer(person) {
				if (person.age >= 18) {
					this.players.push(person);
					return true;
				}
				return false;
			}
			toString() {
				var str = '<strong>' + this.teamName + '</strong>';
				str += '<ul>';
				for (let p in this.players) {
					let person = this.players[p];
					str += '<li>' + person + '</li>';
				}
				str += '</ul>';
				return str;
			}
		}
	</script>
</head>
<body>
	<h1>Multiple Classes with Inheritance</h1>
	<script>
		var jdoe = new Person('Jane', 'Doe', 32);
		var mcontrera = new Player('Maria', 'Contrera', 47, 2);
		var afung = new Person('Adam', 'Fung', 23);
		
		var Tigers = new Team('The Tigers', 'soccer');
		if (!Tigers.addPlayer(jdoe)) {
			alert('player not added - too young');
		}
		if (!Tigers.addPlayer(mcontrera)) {
			alert('player not added - too young');
		}
		if (!Tigers.addPlayer(afung)) {
			alert('player ' + afung + ' not added - too young');
		}

		document.write('Here is my team:<br><br>');
		document.write(Tigers);
	</script>
</body>
</html>

Code Explanation

Our Person class worked great to represent the people we added to an object of class Team. But, after thinking on it some more, we decided it would make sense to include a jersey number for each team member. Adding a jerseyNumber field to class Person didn't really make sense - it isn't really applicable to generic "people" - so creating a subclass Player seemed the best way to go.

Thus we create class Player which inherits from class Person. Any object of class Player includes the same (fname, lname, and age) properties as a Person, but now includes a new, specific property jerseyNumber. Class Player overrides class Person's constructor to allow for the setting of this property, and overrides the toString() method to display the player's jersey number.

Note that we create two Person objects (jdoe and afung) and one Player object (mcontrera) and add each to the team. Our Team object, Tigers, is happy to accept any flavor of object (Person or Player) to be added as a team member. Interestingly, when we build the unordered list of players inside the Team-class toString() method, JavaScript "knows" which toString() method to use, Person or Player: Maria Contrera's bullet shows her jersey number, while the other two team members' bullets do not.

Maps

So far in this lesson we've written prototypes (classes) to create our own custom objects, with properties and methods specific to our needs. We'll look now at a few built-in objects from JavaScript that may be useful to you, the first of which is the Map object. Similar to the built-in Object, a Map is a collection of key-value pairs. But, unlike an Object, a Map allows for any data type as a key, not just Strings and Symbols. So a key could be an array, an instance of a custom class, etc.

Below are listed common Map properties and methods:

Common Map Properties
Property Description Example
constructor Creates a Map theMap = new Map()
size Returns the number of elements theMap.size
Common Map Methods
Method Description Example
clear Removes all elements theMap.clear()
delete Removes a specified element theMap.delete(16) //deletes element with index 16
forEach Iterates over the Map theMap.forEach(function (value, key, map) { //do something with each key/value })
get Returns element from specified key theMap.get("arizona") // gets value of element with key "arizona"
has Returns true if key exists theMap.has(20) // returns true if key 20 exists
set Adds element or modifies existing element theMap.set(-9, "blue") // adds element "blue" with key -9, or changes value of existing key
toString Returns string representation theMap.toString()

Let's look at a simple example:

Code Sample:

AdvancedObjects/Demos/map.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Map</title>
</head>
<body>
	<h1>Map</h1>
	<script>
		class Person {
			constructor(fname, lname, age) {
				this.fname = fname;
				this.lname = lname;
				this.age = age;
			}
			toString() {
				return this.fname + ' ' + this.lname;
			}
		}

		var p = new Person('Jane','Doe',31);
		var arr = ['one', 'two', 'three'];
		var myMap = new Map();
		myMap.set(1, "black");
		myMap.set("lemonade", 2);
		myMap.set(p, 3);
		myMap.set(47, arr);
		myMap.set(1, "red");

		document.write('<ul>');
		myMap.forEach(function (value, key, map) {
			document.write('<li>');
			document.write(key.toString() + ' - ' + value.toString());
			document.write('</li>');
		});
		document.write('</ul>');

		document.write('<p>Size of map: ' + myMap.size + '</p>');
		document.write('<p>Get element with Person key: ' + myMap.get(p) + '</p>');
		document.write('<p>Does myMap have element with key Person p?: ' + myMap.has(p) + '</p>');
		document.write('<p>Does myMap have element with key 15?: ' + myMap.has(15) + '</p>');
		myMap.clear();
		document.write('<p>(myMap cleared)</p>');
		document.write('<p>Size of map: ' + myMap.size + '</p>');
	</script>
</body>
</html>

Code Explanation

We create a new Map on line 23 and add elements to it with the set method. As you can see, keys for our Map (myMap) can be an object of class Person or an array, in addition to numbers and strings.

We can also create a Map object from any iterable object when calling the constructor. An iterable object is any object that implements the Iterable interface - data types like Array, String, String, and Map (itself) are all examples of iterable objects in JavaScript. If we return the iterator object associated with the iterable object, we can create a Map from the original object.

Thus we can create a new Map with code like this:

var arr = [1, 2, 3];
var m = new Map(arr.entries());

where entries() returns the iterator object from the array. For a string, we could do the following:

var str = "abc";
var m = new Map(Array.from(str).entries());

Map vs Object?

Because JavaScript already offers a built-in data structure (Object) for associative arrays, a reasonable question is "why use maps at all?" The ability to use non-primitive data as keys is one answer to this question; as you saw in the example above, we can use an object from a custom class (like Person) or an array as the key for a Map, which we can't do for an Object.

Another reason to use a Map is that, unlike with Objects, the built-in iterator (forEach) offers an easy way to loop over the collection housed in the Map object, and those key/value pairs are returned by the iterator in insertion order (that is, in the order in which we added them), which isn't necessarily true for iterating over an Object. Furthermore, the size property makes it considerably easier to find the length of a Map, which is more difficult with an Object.

Sets

Another useful JavaScript object is the Set. Unlike a Map, a Set object can contain no duplicate items: it stores only unique values, of any type. As with Maps, we can iterate over a Set object in insertion order.

Common Set Properties
Property Description Example
constructor Creates a Set theSet = new Set()
size Returns the number of values theSet.size
Common Set Methods
Method Description Example
add Appends a new value, if the value does not already exist in the set theSet.add('abc')
clear Removes all values theSet.clear()
delete Removes a specified value theSet.delete('abc')
forEach Iterates over the Set theSet.forEach(function (item) { //do something with each item })
has Returns true if is element is contained in the set theSet.has(20) // returns true if 20 exists in theSet

We'll take a look now at an example:

Code Sample:

AdvancedObjects/Demos/set.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Set</title>
</head>
<body>
	<h1>Set</h1>
	<script>
		var mySet = new Set();
		mySet.add(1);
		mySet.add("lemonade");
		var arr = ['one', 'two', 'three'];
		mySet.add(arr);
		mySet.add(1); // won't be added, as 1 already exists as an element

		document.write('<p>Size of set: ' + mySet.size + '</p>');
		document.write('<p>Does mySet have element 1?: ' + mySet.has(1) + '</p>');
		document.write('<p>Does mySet have element arr?: ' + mySet.has(arr) + '</p>');
		document.write('<p>Does mySet have element "blue?: ' + mySet.has("blue") + '</p>');
		mySet.clear();
		document.write('<p>(mySet cleared)</p>');
		document.write('<p>Size of set: ' + mySet.size + '</p>');
	</script>
</body>
</html>

Code Explanation

We create a new Set on line 10 and add values to it with the add method. Note that adding the value 1 on line 15 has no effect, since 1 was already added previously.

Next, we'll ask you to try out using Map and Set in the following exercise:

Guessing Game with Map and Set

Duration: 20 to 30 minutes.

In this exercise, you will create a guessing game using the Map and Set objects.

  1. Open AdvancedObjects/Exercises/guessinggame.html for editing.
  2. The game selects a random word (from the possibleWords array), creates wordToGuess as a Map from the word, creates userGuess as a Map (of the same size) with all elements initially as a space, and creates a Set (lettersNotGuessed) of all 26 alphabet letters.
  3. Users play the game by clicking on a letter from the displayed list. Correct guesses show in the blank-character spaces for the word to guess; guessed letters (correct or incorrect) disappear from the list.
  4. The HTML markup, CSS styling, and event-handling JavaScript code is done for you.
  5. Your tasks are to complete the displayWord function (which displays the word to guess), isWin function (which returns true when the user has won), and doGuess function (which processes each guess).
  6. Test your solution in a browser.

Solution:

AdvancedObjects/Solutions/guessinggame.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Guessing Game</title>
	<style>
		body {
			background:#ccc;
		}
		#main {
			background:#eee;
			width:79%;
			padding:2% 5%;
			margin:20px auto;
		}
		h1 {
			margin:0 0 10px 0;
		}
		div#word {
			float:left;
			width:30%;
		}
		div#word div  {
			background:#fff;
			height:35px;
			width:30px;
			line-height:30px;
			font-size:30px;
			float:left;
			margin-right:5px;
			border:1px solid #000;
			text-align: center;
		}
		div#letters {
			float:right;
			width:69%;
		}

		div#letters a {
			display:block;
			padding:5px;
			font-size:28px;
			color:#000;
			text-decoration: none;
			float:left;
		}
		footer {
			clear:both;
			text-align: center;
			font-size:30px;
		}
	</style>
	<script>
		var lettersNotGuessed, wordToGuess, userGuess;

		function displayWord(userGuessMap) {
			var wordHTML = '';
			userGuessMap.forEach(function (value, key, map) {
				wordHTML += '<div>' + value + '</div>';
			});
			document.getElementById('word').innerHTML = wordHTML;
		}

		function displayLettersToGuess(lettersNotGuessedSet) {
			var lettersHTML = '';
			for (let letter of lettersNotGuessedSet) {
				lettersHTML += '<a href="#" id="' + letter + '" class="letter">' + letter + '</a>';
			}
			document.getElementById('letters').innerHTML = lettersHTML;
		}

		function isWin(userGuessMap) {
			var is_winner = true;
			userGuessMap.forEach(function (value, key, map) {
				if (value == " ") {
					is_winner = false;
				}
			});
			return is_winner;
		}

		function doGuess(e) {
			e.preventDefault();
		    var letter = e.target.getAttribute("id");

		   lettersNotGuessed.delete(letter);
		    displayLettersToGuess(lettersNotGuessed);

		    wordToGuess.forEach(function (value, key, map) {
				if (value == letter) {
					userGuess.set(key, letter);
				}
			});
			displayWord(userGuess);

			if (isWin(userGuess)) {
				document.getElementById('footer').innerHTML = 'You Win!';
			}
		};

		window.onload = function() {
			var possibleWords = ['apple','banana','pear'];
			var wordToGuessString = possibleWords[Math.floor(Math.random()*possibleWords.length)];
			wordToGuess = new Map(Array.from(wordToGuessString).entries());
			userGuess = new Map();
			for(let i=0; i<wordToGuess.size; i++) {
				userGuess.set(i, ' ');
			}
			displayWord(userGuess);
			lettersNotGuessed = new Set('abcdefghijklmnopqrstuvwxyz');
			displayLettersToGuess(lettersNotGuessed);

			document.querySelector('body').addEventListener('click', function(e) {
				if (e.target.tagName.toLowerCase() === 'a') {
					doGuess(e);
				}
			});
		}
	</script>
</head>
<body>
	<div id="main">
		<h1>Guessing Game</h1>
		<div id="word"></div>
		<div id="letters"></div>
		<footer id="footer"></footer>
	</div>
</body>
</html>

Code Explanation

We use forEach in the displayWord function to build a series of divs containing, initially, just spaces; this displays the empty boxes representing the word to guess.

Similarly, we iterate over userGuess to check if there are any spaces (the ' ' character) left; if so, the user has not yet won and we return false.

In function doGuess, we remove the guessed letter from the lettersNotGuessed set, call displayLettersToGuess to update the set of letters still available, iterate over wordToGuess and update userGuess if the user has guessed a letter (or multiple instances of the same letter) correctly, and call displayWord to update any already-guessed letters.

Modules

It's easy to overwrite variables and functions in JavaScript. Really easy. Because most of the JavaScript we write gets executed in a web browser in a single global context, avoiding conflicts between same-named variables can be a challenge. One strategy to address this concern is modules, a feature introduced (in this fashion) in ES2015.

Modules have existed in JavaScript for a while; the CommonJS and Asynchronous Module Definition (AMD) API standards both offered a way for JavaScript developers to use modules. ES2015 brings the module feature into the language officially.

Browser Support for ES2015 Modules

Unforunately, no current browser supports ES2015 modules. But given the importance of modules, we give a brief overview of the concepts here - and offer examples of both ES2015 code (which won't work natively) and examples transpiled into ES5 code, which will work. (Transpiling is the process of converting source code of one language or version of a language into another language/version. Please see https://en.wikipedia.org/wiki/Source-to-source_compiler for more information.)

Module Overview

A module in ES2015 is a file that exports something - functions, constants, etc. - for importing by other files. Modules have their own scope, meaning that they don't share the global scope; you have to import anything needed in the module explicitly. As a result, the contents of each module is, of course, modular: self-contained and free from any possible side effects from conflicts with other code.

As a simple example, consider a file mod1.js that contains the following code:

export const val = 1234;
export function doubleIt(x) {
	return 2 * x;
}

export function tripleIt(x) {
	return 3 * x;
}

Our module defines a constant (val) and two functions, doubleIt and tripleIt. In this example, we're using named exports, in which we prefix each declaration with the export keyword.

We might use the resources created in our mod1.js module in another JavaScript file named main.js:

import { val, doubleIt } from './mod1';
document.write(val);
document.write(doubleIt(4));

The import statement at the top of main.js allows us to use the val constant and doubleIt function from the mod1.js module. We can then use resources just as if they were declared in main.js itself. We could have, of course, chosen to import all of the exported items in main.js from mod1.js:

import { val, doubleIt, tripleIt } from 'mod1';
//...

We can also import all of the module's exports and refer to them using property notation:

import * as mod1 from './mod1';
document.write(mod1.val);
document.write(mod1.doubleIt(4));

In the module, we can change the name of the export in mod1.js:

const val = 1234;
export {val as VAL1};

which we then import using that name:

import { VAL1 } from './mod1';
//...

Similarly, we can import under a different name; in main.js, this would be:

import { VAL1 as THEVAL } from './mod1';
document.write(THEVAL);

Modules also allow for default exports in which we export a single value, often a constructor for a class with one model per module. A module can pick a default export - only one per module file, where the entire module "is" a single function. Here's mod1.js:

export default function () { ... };

We can then make use of the exported function in main.js with the following code:

import mod1 from 'mod1';
mod1();

Modules Example

As discussed above, ES2015 modules syntax isn't supported by any current browsers. But we can make use of the TypeScript transpiler and the webpack module bundler to convert ES2015 module code into ES5 compatible JavaScript. (See https://www.typescriptlang.org/ and https://webpack.github.io/, respectively, for more information.)

Check out the files in the directory AdvancedObjects/Demos/modules/ to view our example. The ES2015 module is the file module1.ts; the ".ts" file extension indicates that this is a TypeScript (a superset of JavaScript) file. Our module exports a single function, randomInt:

export function randomInt(fromNum, toNum) {
	return Math.floor(Math.random() * toNum) + fromNum;
}

The JavaScript file app.ts (again, a TypeScript file) imports the function from the module:

import { randomInt } from './module1';

var random = randomInt(0, 100);
console.log(random);

We import the function randomInt from the module, call the function, and write the value to the console.

To transpile each TypeScript file (module1.ts, app.ts) to their equivalent JavaScript file (module1.js, app.js), we run the following from the command line from inside the modules directory:

tsc module1.ts
tsc app.ts

We use webpack to bundle the code into bundle.js; to do this, we run the following from the command line:

npm install

to load the needed dependency, webpack. We then run the following from the command line to update bundle.js:

node_modules/.bin/webpack app.js bundle.js

We can then open index.html in a browser to run the code.

Given the lack of current browser support for ES2015 modules, we need to do considerable work to get our code to work! But given that modules are an important feature in future front-end development, we think it worth it for you to be exposed to this functionality.