Essential JavaScript and jQuery
- 3/15/2013
- Before you begin
- Lesson 1: Creating JavaScript objects
- Lesson 2: Working with jQuery
- Practice exercises
- Suggested practice exercises
- Answers
Lesson 1: Creating JavaScript objects
In JavaScript, everything is an object. Strings, numbers, and functions are all objects. You have learned how to create functions, so you already have exposure to creating objects, as you see in this lesson.
Using object-oriented terminology
In many object-oriented languages, when you want to create objects, you start by creating a class, which is a blueprint for an object. Like a blueprint for a house, the blueprint isn’t the house; it’s the instructions that define the type of object that you will be constructing, which is the house. By using a house blueprint, you can create, or construct, many houses that are based on the blueprint. Each house is an object of type house, also known as an instance of the house type.
The developer writes the class, which is then used to construct objects. In a baseball application, you might create a Player (classes are normally capitalized) class that has properties for first and last name, batting average, error count, and so on. When you create your team, you might use the Player class to create nine Player objects, each having its own properties. Each time you construct a Player object, memory is allocated to hold the data for the player, and each piece of data is a property, which has a name and a value.
The three pillars of object-oriented programming are encapsulation, inheritance, and polymorphism. Encapsulation means that you hide all details except those that are required to communicate with your object in order to simplify the object for anyone using the object. Inheritance means that you can create an “is a” relationship between two classes, in which the child class automatically inherits everything that is in the parent class. Polymorphism means that you can execute a function on the parent class, but the behavior changes (morphs) because your child class has a function that overrides the function in the parent class.
The parent class is also known as the base class, the super class, or the generalized class. The child class is also known as the derived class, the subclass, or the specialized class. Because it’s easy to think of actual children inheriting from parents, the terms parent and child are usually used, but you should remember the other terms for these classes to communicate effectively with others about object-oriented programming.
In object-oriented programming, objects can have data implemented as properties and behaviors implemented as methods. A property is essentially a variable that is defined on an object and owned by the object. A method is a function that is defined on an object and owned by the object.
Understanding the JavaScript object-oriented caveat
JavaScript is a very flexible language. You can create objects, but the relationship between the JavaScript language and class-based, object-oriented programming is not direct. The most glaring example is that there is no class keyword in JavaScript. If you’re familiar with class-based, object-oriented programming, you’ll be struggling to find the “class.”
JavaScript is a prototype-based, object-oriented programming language. In JavaScript, everything is an object, and you either create a new object from nothing, or you create an object from a clone of an existing object, known as a prototype.
Conceptually, you can simulate class creation by using a function. Class-based, object-oriented purists dislike the idea of a function being used to simulate a class. Keep an open mind as patterns are presented. This lesson should give you what you need to accomplish your tasks.
The problem you typically encounter is finding one correct solution for all scenarios. As you read on, you’ll find that achieving proper encapsulation of private data requires you to create copies of the functions that can access the private data for each object instance, which consumes memory. If you don’t want to create copies of the method for each object instance, the data needs to be publicly exposed, thus losing the benefits of encapsulation, by which you hide object details that users shouldn’t need to see.
The general consensus of this issue of encapsulation versus wasteful memory consumption is that most people would rather expose the data to minimize memory consumption. Try to understand the benefits and drawbacks of each pattern when deciding which option to implement in your scenario.
Using the JavaScript object literal pattern
Probably the simplest way to create an object in JavaScript is to use the object literal syntax. This starts with a set of curly braces to indicate an object. Inside the curly braces is a comma-separated list of name/value pairs to define each property. Object literals create an object from nothing, so these objects contain precisely what you assign to them and nothing more. No prototype object is associated with the created object. The following example demonstrates the creation of two objects that represent vehicles.
var car1 = { year: 2000, make: 'Ford', model: 'Fusion', getInfo: function () { return 'Vehicle: ' + this.year + ' ' + this.make + ' ' + this.model; } }; var car2 = { year: 2010, make: 'BMW', model: 'Z4', getInfo: function () { return 'Vehicle: ' + this.year + ' ' + this.make + ' ' + this.model; } };
In this example, public properties are created for year, make, model, and getInfo. The getInfo property doesn’t contain data; it references an anonymous function instead, so getInfo is a method. The method uses the this keyword to access the data. Remember that the this keyword references the object that owns the code where the this keyword is. In this case, the object is being created. If the this keyword were omitted, the code would look in the global namespace for year, make, and model.
To test this code, the following QUnit test checks to see whether each object contains the data that is expected.
test("Object Literal Test", function () { expect(2); var expected = 'Vehicle: 2000 Ford Fusion'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var expected = 'Vehicle: 2010 BMW Z4'; var actual = car2.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
This test performs an assertion by using the car1 variable and then performs another assertion by using the car2 variable. The successful test is shown in Figure 6-1.
If you want to define an array of items and assign it to a property, you can use square brackets as shown in the following example.
var car1 = { year: 2000, make: 'Ford', model: 'Fusion', repairs: ['repair1', 'repair2', 'repair3'], getInfo: function () { return 'Vehicle: ' + this.year + ' ' + this.make + ' ' + this.model; } };
Because this is one of the easiest ways to create an object, you’ll probably use it to gather data to send to other code. In this example, two instances of a type Object are created, and properties are dynamically added to each instance. This does not create a Vehicle type.
Figure 6-1 The JavaScript object literal test
Creating dynamic objects by using the factory pattern
In addition to using the JavaScript literal object syntax, JavaScript has an Object type, and you can use it to create an object programmatically. Object has a prototype object that is cloned when you use the new keyword to create a new Object instance. The prototype object has the following inherited methods.
- constructor The function that is called to initialize a new object
- hasOwnProperty Returns a Boolean indicator of whether the current object has the specified property
- isPrototypeOf Returns a Boolean indicator of whether the current object is in the specified object’s prototype object chain
- propertyIsEnumerable Returns true if the object can be enumerated in a for...in loop
- toLocalString Converts a date to a string value based on the current local
- toString Returns the string representation of the current object
- valueOf Returns the value of the current object converted to its most meaningful primitive value
After the object is created, you can dynamically add properties to it that hold the data and reference functions. You can wrap this code in a function that returns the object as shown in the following code example.
function getVehicle(theYear, theMake, theModel) { var vehicle = new Object(); vehicle.year = theYear; vehicle.make = theMake; vehicle.model = theModel; vehicle.getInfo = function () { return 'Vehicle: ' + this.year + ' ' + this.make + ' ' + this.model; }; return vehicle; }
This code takes advantage of JavaScript’s dynamic nature to add year, make, model, and getInfo to the object and then returns the object. Placing this code in a function makes it easy to call the getVehicle function to get a new object. The encapsulation of the code to create an object is commonly referred to as using the factory pattern. Can you create multiple instances of vehicle? You can create multiple instances of Object and add properties dynamically to each instance, but the actual type is Object, not vehicle. The following QUnit test demonstrates the creation of multiple instances.
test("Create Instances Test Using Factory Pattern", function () { expect(2); var car1 = getVehicle(2000, 'Ford', 'Fusion'); var car2 = getVehicle(2010, 'BMW', 'Z4'); var expected = 'Vehicle: 2000 Ford Fusion'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var expected = 'Vehicle: 2010 BMW Z4'; var actual = car2.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
This might be all you need when you are gathering some data to put into an object structure and pass to some other code or service. Although the getVehicle function encapsulates the object creation, the properties are all public. This can be desirable in some scenarios, but if you want the data to be private, this approach won’t work. Like when using the literal object syntax, you might encounter the problem that every vehicle’s type is Object, and you might want to create a Vehicle class to have a named Vehicle type.
Creating a class
There is no class keyword in JavaScript, but you can simulate a class by starting with a function, which is actually the constructor function of the object. Consider the following function.
function Vehicle(theYear, theMake, theModel) { year = theYear; make = theMake; model = theModel; getInfo = function () { return 'Vehicle: ' + year + ' ' + make + ' ' + model; }; }
There are several problems with this code. All the variables are defined without the var keyword, so year, make, model, and getInfo are automatically defined in the global scope and are accessible from anywhere. The following is a passing QUnit test that initializes Vehicle and calls the getInfo method to retrieve the data.
test("Function Test", function () { expect(2); Vehicle(2000, 'Ford', 'Fusion'); var expected = 'Vehicle: 2000 Ford Fusion'; var actual = getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); expected = 2000; actual = year; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
The Vehicle function accepts three parameters and doesn’t return anything. Instead, it is setting global variables, and there is no provision for multiple instances. To prove that global variables are being set, the second assertion is checking to see whether there is a global variable named year that equals 2,000. This assertion succeeds, which proves that the data is not encapsulated, and there is only one copy of the data. For example, the following QUnit test fails.
test("Failing Function Test", function () { expect(1); Vehicle(2000, 'Ford', 'Fusion'); Vehicle(2010, 'BMW', 'Z4'); var expected = 'Vehicle: 2000 Ford Fusion'; var actual = getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); expected = 2000; actual = year; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
Figure 6-2 shows the failures. The problem is that year, make, and model of the second vehicle replaced year, make, and model of the first vehicle. The variable getInfo was also replaced, but instead of holding data, it holds a reference to the function code. The getInfo variable’s value was replaced with new function code; it just happened to be the same code. Once again, there is no encapsulation.
Figure 6-2 The failing test assertions after a second vehicle is used
To solve the problem, you want to implement encapsulation. Then you need to create objects, each with its own data. To implement encapsulation, use the var keyword for the year, make, and model. This will make these variables private to the function. Notice that the var keyword is not used with getInfo because the getInfo variable needs to be public to be called from outside the object, but you don’t want the getInfo variable to be global. Assign getInfo to the current object by using the this keyword. The result is a class that encapsulates the data and exposes getInfo to retrieve the data in a controlled way as follows.
function Vehicle(theYear, theMake, theModel) { var year = theYear; var make = theMake; var model = theModel; this.getInfo = function () { return 'Vehicle: ' + year + ' ' + make + ' ' + model; }; }
Remember that the this keyword references the object that owns the current code. The way the test is currently written, the this keyword references the global object, and getInfo will still be a global variable. To solve the problem, the new keyword must be used to create an object from this class, as shown in the modified test code.
test("Encapsulation Test", function () { expect(2); var car1 = new Vehicle(2000, 'Ford', 'Fusion'); var car2 = new Vehicle(2010, 'BMW', 'Z4'); var expected = 'Vehicle: 2000 Ford Fusion'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); expected = 2000; actual = year; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
Notice that a new variable is defined, car1, and it is assigned the object that is created by using the new keyword. After that, another new variable is defined, car2, and it is assigned the second Vehicle object created by using the new keyword. Two instances of the Vehicle class are being created, which means that two Vehicle objects are being constructed. Each instance has its own data and its own copy of the getInfo method. The getInfo method is public but has access to the private data. A method that is public but has access to private data is called a privileged method.
Figure 6-3 shows the test results. Notice that the first assertion passed, which proves that there are separate object instances, each having its own data. The second assertion failed. The failure message states that the year is undefined, which proves that the year is not directly accessible from the test, which is in the global namespace. Instead, year, in addition to make and model, is encapsulated in the object.
You have now created a class and constructed objects from the class, but there’s more to cover in the Vehicle function that is being used as a class. The Vehicle function is known as a constructor function. The new keyword created an object and executed the constructor function to initialize the object by creating the year, make, and model private variables and the public getInfo variable. Each instance has these four variables, and memory is allocated for them. That’s what you want for the data, but is that what you want for the getInfo variable that references a function? The answer is that it depends on what you are trying to accomplish with your code.
Figure 6-3 Successful first assertion and failed second assertion
Consider the following test code that creates two Vehicle objects, but then replaces the code in getInfo of the first Vehicle object with different code. Does this replace the code in the second Vehicle object?
test("Function Replacement Test", function () { expect(2); var car1 = new Vehicle(2000, 'Ford', 'Fusion'); var car2 = new Vehicle(2010, 'BMW', 'Z4'); car1.getInfo = function () { return 'This is a Car'; }; var expected = 'This is a Car'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var expected = 'This is a Car'; var actual = car2.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
The test result is shown in Figure 6-4. The first assertion succeeded, which proves that the function was successfully replaced on the first Vehicle object. The second assertion failed, which proves that the second Vehicle object’s getInfo function was not replaced. Is that what you expected? Is that what you wanted? You can see that in some scenarios, this behavior is desirable, but in other scenarios, you might have wanted to replace the function across all objects. To do this, you use the prototype pattern.
Figure 6-4 Successful first assertion, proving that the function was replaced; failed second assertion, proving that the second Vehicle’s function was not replaced
Using the prototype property
In JavaScript, everything, including the function, is an Object type, which has a prototype property. The prototype itself is an object containing properties and methods that should be available to all instances of the type you’re working with. However, this prototype is typically specified externally to the constructor function, so the prototype doesn’t have access to private variables. Therefore, you must expose the data for the prototype to work. The following is an example of using the prototype property to create a single getInfo method that is shared across all instances.
function Vehicle(theYear, theMake, theModel) { this.year = theYear; this.make = theMake; this.model = theModel; } Vehicle.prototype.getInfo = function () { return 'Vehicle: ' + this.year + ' ' + this.make + ' ' + this.model; }
By using this class and the prototype, you can write the following test to ensure that each instance has its own data and that the getInfo function works properly.
test("Instance Test Using Prototype", function () { expect(2); var car1 = new Vehicle(2000, 'Ford', 'Fusion'); var car2 = new Vehicle(2010, 'BMW', 'Z4'); var expected = 'Vehicle: 2000 Ford Fusion'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var expected = 'Vehicle: 2010 BMW Z4'; var actual = car2.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
In this test, two instances of the Vehicle class are created, each having different data. The first assertion calls getInfo on car1 and verifies that the proper result is returned. The second assertion calls getInfo on car2 and verifies that the proper result is returned. The result is shown in Figure 6-5.
Figure 6-5 The modified class using the prototype property to create the getInfo function
Now that you have a functioning class, change the prototype to see whether it can be changed across all instances.
test("Instance Test Using Prototype Replace Function", function () { expect(2); var car1 = new Vehicle(2000, 'Ford', 'Fusion'); var car2 = new Vehicle(2010, 'BMW', 'Z4'); Vehicle.prototype.getInfo = function () { return 'Car: ' + this.year + ' ' + this.make + ' ' + this.model; } var expected = 'Car: 2000 Ford Fusion'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var expected = 'Car: 2010 BMW Z4'; var actual = car2.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
This test creates two Vehicle instances and then changes getInfo. Next, the two assertions are modified to check both instances to see whether they are using the updated getInfo. The result is shown in Figure 6-6.
Figure 6-6 The modification of the getInfo prototype affected all instances
You might use the prototype property when creating functions that will be shared across all instances, but remember that the prototype is defined externally to the constructor function, so all properties must be public when using the this keyword. If you don’t need to replace individual instance functions and you don’t mind making your data public, the prototype is efficient.
Debating the prototype/private compromise
You’ve learned the primary patterns for creating a JavaScript object, but there can be a compromise in which you can have private data that is readable by creating a method for retrieving the data, also known as a getter, which has no setter, a method for setting the value. This would require you to write a function that is copied for each object, but you should keep the function as small as possible, as shown in the following code example.
function Vehicle(theYear, theMake, theModel) { var year = theYear; var make = theMake; var model = theModel; this.getYear = function () { return year; }; this.getMake = function () { return make; }; this.getModel = function () { return model; }; } Vehicle.prototype.getInfo = function () { return 'Vehicle: ' + this.getYear() + ' ' + this.getMake() + ' ' + this.getModel(); }
The QUnit test for this code creates two instances of Vehicle and, for each assertion, executes the getInfo method of each object and checks for the proper value. The test is as follows.
test("Instance Test Using Prototype and getters", function () { expect(2); var car1 = new Vehicle(2000, 'Ford', 'Fusion'); var car2 = new Vehicle(2010, 'BMW', 'Z4'); var expected = 'Vehicle: 2000 Ford Fusion'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var expected = 'Vehicle: 2010 BMW Z4'; var actual = car2.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
This test is successful, so replace the getInfo method and add more tests. The following test code does this.
test("Instance Test Using Prototype and getters", function () { expect(4); var car1 = new Vehicle(2000, 'Ford', 'Fusion'); var car2 = new Vehicle(2010, 'BMW', 'Z4'); var expected = 'Vehicle: 2000 Ford Fusion'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var expected = 'Vehicle: 2010 BMW Z4'; var actual = car2.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); Vehicle.prototype.getInfo = function () { return 'Car Year: ' + this.getYear() + ' Make: ' + this.getMake() + ' Model: ' + this.getModel(); }; var expected = 'Car Year: 2000 Make: Ford Model: Fusion'; var actual = car1.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var expected = 'Car Year: 2010 Make: BMW Model: Z4'; var actual = car2.getInfo(); equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
The test result is shown in Figure 6-7. You can replace the getInfo method and, because the data is exposed as read-only, it’s available to be used in the new method. In addition, the privileged getters are small, which minimizes the amount of memory consumed when each instance has a copy of the method. Remember to create only getter methods as needed and to keep them small and concise.
Figure 6-7 The use of getters to expose read-only data as a good compromise
Implementing namespaces
One problem to watch for is the pollution of the global namespace. As your program gets larger and libraries are added, more entries are added to the global object. How can you minimize this global namespace pollution?
JavaScript doesn’t have a namespace keyword, but you can implement the equivalent of a namespace by using techniques that are similar to those used to create objects. Consider the following code sample.
var vehicleCount = 5; var vehicles = new Array(); function Car() { } function Truck() { } var repair = { description: 'changed spark plugs', cost: 100 };
This code sample places five entries in the global namespace, and as the application grows, this global namespace pollution also grows. You can implement the namespace pattern to solve the problem. The following example shows the creation of an object that contains the five items from the previous example.
var myApp = {}; myApp.vehicleCount = 5; myApp.vehicles = new Array(); myApp.Car = function () { } myApp.Truck = function () { } myApp.repair = { description: 'changed spark plugs', cost: 100 };
In this sample, myApp is the only entry in the global namespace. It represents the name of the application and its root namespace. Notice that object literal syntax is used to create an empty object and assign it to myApp. Everything else is added to the object. Sub-namespaces can also be created and assigned to myApp.
You can see that a namespace was created by creating an object. Although only one entry is made in the global namespace, all the members of myApp are globally accessible. In addition, if you create a namespace for your application, and your application has many JavaScript files, you might want to have logic to create the namespace object only if it hasn’t been created. In the following example, the code for myApp is modified to create the namespace object if it doesn’t already exist. This code uses the OR operator to create a new object if myApp does not have a value.
var myApp = myApp || {};
You can use the object techniques already defined in this lesson to make some members of the namespace private and some public. The difference is that the namespace is a singleton object, so you create a single instance for the namespace. You don’t need to worry about functions defined in the constructor function consuming additional memory for each instance because there is only one instance. Here is an example of the use of an immediately invoked function expression (IIFE) to create the myApp namespace in which Car and Truck are public, but vehicleCount, vehicles, and repair are private.
(function () { this.myApp = this.myApp || {}; var ns = this.myApp; var vehicleCount = 5; var vehicles = new Array(); ns.Car = function () { } ns.Truck = function () { } var repair = { description: 'changed spark plugs', cost: 100 }; }());
An IIFE (pronounced iffy) is an anonymous function expression that has a set of parentheses at the end of it, which indicates that you want to execute the function. The anonymous function expression is wrapped in parentheses to tell the JavaScript interpreter that the function isn’t only being defined; it’s also being executed when the file is loaded.
In this IIFE, the first line creates the myApp namespace if it doesn’t already exist, which represents the singleton object that is used as the namespace. Next, an ns variable (for namespace) is created as an alias to the namespace to save typing within the IIFE, so ns can be used in place of this.myApp. After that, the private members of the namespace are defined by using the var keyword. Car and Truck are public, so they are prefixed with ns.
If you’re wondering how you would create a sub-namespace under myApp, the following example shows how you can add a billing namespace under the myApp namespace.
(function () { this.myApp = this.myApp || {}; var rootNs = this.myApp; rootNs.billing = rootNs.billing || {}; var ns = rootNs.billing; var taxRate = .05; ns.Invoice = function () { }; }());
This example also implements an IIFE to create the namespace. First, the myApp namespace is created if it doesn’t already exist and is assigned to a local rootNs variable to save typing inside the namespace. Next, the billing namespace is created and assigned to the local ns variable to save typing inside the namespace. Finally, the private taxRate property is defined while the public Invoice is defined.
Implementing inheritance
JavaScript provides the ability to implement inheritance, which is useful when you can define the relationship between two objects as an “is a” relationship. For example, an apple is a fruit, an employee is a person, and a piano is an instrument. You look for “is a” relationships because they provide an opportunity to implement code reuse. If you have several types of vehicles, you can create Vehicle with the common vehicle traits defined in it. After Vehicle is created, you can create each vehicle type and inherit from Vehicle so you don’t need duplicate code in each vehicle type.
As an example of inheritance, start by defining the base class. Using the Vehicle example, the following is an example of a Vehicle base class.
var Vehicle = (function () { function Vehicle(year, make, model) { this.year = year; this.make = make; this.model = model; } Vehicle.prototype.getInfo = function () { return this.year + ' ' + this.make + ' ' + this.model; }; Vehicle.prototype.startEngine = function () { return 'Vroom'; }; return Vehicle; })();
This class is wrapped in an IIFE. The wrapper encapsulates the function and the Vehicle prototype. There is no attempt to make the data private. The code works as follows.
- When the code is loaded into the browser, the IIFE is immediately invoked.
- A nested function called Vehicle is defined in the IIFE.
- The Vehicle function’s prototype defines getInfo and startEngine functions that are on every instance of Vehicle.
- A reference to the Vehicle function is returned, which is assigned to the Vehicle variable.
This is a great way to create a class, and all future class examples use this pattern. To create Vehicle objects, you use the new keyword with the Vehicle variable. The following test creates an instance of Vehicle and tests the getInfo and startEngine methods.
test('Vehicle Inheritance Test', function () { expect(2); var v = new Vehicle(2012, 'Toyota', 'Rav4'); var actual = v.getInfo(); var expected = '2012 Toyota Rav4'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = v.startEngine(); var expected = 'Vroom'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
Now that you have a Vehicle parent class with three properties and two methods, you can create child classes for Car and Boat that inherit from Vehicle. Start by writing an IIFE but, this time, pass Vehicle into the IIFE as follows.
var Car = (function (parent) { })(Vehicle);
Because Vehicle in this example is the Vehicle variable, not the Vehicle function, Car needs to be defined after Vehicle. Vehicle is passed into the IIFE and is available inside the IIFE as parent. Next, the function for Car can be added inside the IIFE. Inside the function, add any additional properties, such as wheelQuantity, and initialize to four. In the function, call the parent class’s constructor for Car to allocate memory slots for the year, make, and model. To call the parent constructor function, use a call method that exists on the Function object, which accepts a parameter for the this object, and parameters for the parameters on the function being called, as follows.
var Car = (function (parent) { function Car(year, make, model) { parent.call(this, year, make, model); this.wheelQuantity = 4; } return Car; })(Vehicle);
Notice how this example used the call method to modify the this object; the this object is the Car object, so the call to the parent constructor function creates year, make, and model on the Car object. The Function object has another method, apply, that does the same thing, but the extra parameters are passed as an array instead of as a comma-delimited list.
Next, the inheritance must be set up. You might think that you’ve already set up inheritance because the previous example calls the parent class’s constructor, and the year, make, and model are created on Car, but getInfo and startEngine were not inherited. The inheritance is accomplished by changing the Car prototype object to be a new Vehicle object. Remember that the prototype is the object that is cloned to create the new object. By default, the prototype is of type Object. After the new Vehicle is assigned to the prototype, the constructor of that Vehicle is changed to be the Car constructor as follows.
var Car = (function (parent) { Car.prototype = new Vehicle(); Car.prototype.constructor = Car; function Car(year, make, model) { parent.call(this, year, make, model); this.wheelQuantity = 4; } return Car; })(Vehicle);
Finally, you can add more methods into Car. In this example, the getInfo method is added, which replaces the Vehicle getInfo method. The new getInfo gets some code reuse by calling the existing getInfo method on the parent Vehicle object’s prototype. However, you must use the call method and pass the this object as follows.
var Car = (function (parent) { Car.prototype = new Vehicle(); Car.prototype.constructor = Car; function Car(year, make, model) { parent.call(this, year, make, model); this.wheelQuantity = 4; } Car.prototype.getInfo = function () { return 'Vehicle Type: Car ' + parent.prototype.getInfo.call(this); }; return Car; })(Vehicle);
This completes Car, and Boat is similar except that Boat has a propellerBladeQuantity, which is initialized to three, instead of the wheelQuantity property. In addition, getInfo returns the vehicle type of Boat and calls the Vehicle getInfo method as follows.
var Boat = (function (parent) { Boat.prototype = new Vehicle(); Boat.prototype.constructor = Boat; function Boat(year, make, model) { parent.call(this, year, make, model); this.propellerBladeQuantity = 3; } Boat.prototype.getInfo = function () { return 'Vehicle Type: Boat ' + parent.prototype.getInfo.call(this); }; return Boat; })(Vehicle);
In addition to the Vehicle tests already presented, you need to verify the following for the child classes.
- Car and Boat have the inherited year, make, and model properties.
- Car has its wheelQuantity property and it’s set.
- Boat has its propellerBladeQuantity and it’s set.
- Car and Boat return the proper value from the replaced getInfo method.
- Car and Boat return the proper value from the inherited startEngine method.
The following are the Car and Boat tests.
test('Car Inheritance Test', function () { expect(6); var c = new Car(2012, 'Toyota', 'Rav4'); var actual = c.year; var expected = 2012; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = c.make; var expected = 'Toyota'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = c.model; var expected = 'Rav4'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = c.wheelQuantity; var expected = 4; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = c.getInfo(); var expected = 'Vehicle Type: Car 2012 Toyota Rav4'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = c.startEngine(); var expected = 'Vroom'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); }); test('Boat Inheritance Test', function () { expect(6); var b = new Boat(1994, 'Sea Ray', 'Signature 200'); var actual = b.year; var expected = 1994; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = b.make; var expected = 'Sea Ray'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = b.model; var expected = 'Signature 200'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = b.propellerBladeQuantity; var expected = 3; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = b.getInfo(); var expected = 'Vehicle Type: Boat 1994 Sea Ray Signature 200'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); var actual = b.startEngine(); var expected = 'Vroom'; equal(actual, expected, 'Expected value: ' + expected + ' Actual value: ' + actual); });
Figure 6-8 shows the test output. All tests have passed.
Figure 6-8 The passing inheritance tests
Lesson summary
- A class is a blueprint for an object in which an object is an instance of a class.
- The three pillars of object-oriented programming are encapsulation, inheritance, and polymorphism.
- The class from which you inherit is called the parent, base, super, or generalized class. The class that is derived from the parent is called the child, derived, sub, or specialized class. You can implement inheritance by replacing the Child class prototype with a new instance of the parent and replacing its constructor with the Child class constructor function.
- JavaScript is a prototype-based, object-oriented programming language. A prototype is the object used to create a new instance.
- The literal pattern can be used to create an object by using curly braces to create the object. The factory pattern can be used to create a dynamic object.
- JavaScript does not have a class keyword, but you can simulate a class by defining a function.
- Creating private members is possible but usually involves creating privileged getter methods that can be memory consuming.
- The new keyword constructs an object instance.
- The function is an object. The function that simulates a class is called the constructor function.
- Namespaces can be created by using an immediately invoked function expression (IIFE).
Lesson review
Answer the following questions to test your knowledge of the information in this lesson. You can find the answers to these questions and explanations of why each answer choice is correct or incorrect in the “Answers” section at the end of this chapter.
What is the blueprint for an object called?
property
method
class
event
What does JavaScript use as a starting object when constructing a new object?
prototype
property
class
event
How is inheritance supported in JavaScript?
You replace the prototype of the child object with a new instance of the parent object and then replace the prototype constructor with the child constructor.
You call the createChild method on the parent object.
You call the setParent method on the child object.
JavaScript does not support inheritance.