Unlike traditional Object-Oriented Programming languages, JavaScript objects can be thought of as simple collections of name-value pairs. As such, they are similar to:
- Dictionaries in Python
- Hash tables in C and C++
- Associative arrays in PHP
There is a slide deck https://goo.gl/w3jZ3t to accompay this worksheet. Make sure you are familiar with its contents before proceeding.
This chapter covers the following topics:
- Object literals *
- The function object *
- Constructors *
- Immediately invoked function expressions
- Prototypal inheritance
- Classes
Lets start by creating an manipulating objects using object literals. Open the employee.js
file, read through it and see if you can work out what it does. Now run it to see if you were correct.
The simplest way to create new objects is by creating an object literal which is defining an object and storing it in a variable.
const employee = {
firstName: 'Colin',
'last name': 'Stephen'
}
As you can see from the simple example above, the data is stored in name-value pairs, referred to as Properties. This example is defining an object with 3 properties.
The name part of each property is a JavaScript string which may be enclosed in single quotes. These quotes are optional if the property name is a valid JavaScript variable but they are required if this is not the case.
In the example above, firstName
is a valid JavaScript variable but last name
is not because it contains a space which is not allowed in variable names.
It is also possible to create an empty object (we can add properties later). This is done by assigning empty curly braces.
const emptyObject = {}
Here are some valid property names. Notice that both age
and 'age'
are valid.
age
'first name'
'age'
The property names below are not valid because they are not a valid JavaScript variable names.
first name
firstName!
first-name
Whilst it is possible (and useful) to log an entire object to the console, normally we would want to retrieve the values of specific properties.
const employee = {
firstName: 'Colin',
'last name': 'Stephen',
'department': 'Computing'
}
console.log(employee)
const firstName = employee.firstName
const lastName = employee['last name']
const grade = employee.grade
Passing the object name to console.log()
will print out the string representation of the object. To retrieve a specific property value there are two options. If the name is a legal JS variable name the dot .
notation can be used. This is used to log the firstName
property in the example above.
If the name is not a valid JavaScript variable name we need to turn it into a string by using quotes ''
and enclose it in square braces []
. This is used to log the last name
property.
The grade
variable will be undefined
because employee.grade
does not exist. If you want to avoid this and assign a default value if the property is missing you can use the OR operator ||
.
const grade = employee.grade || 'A'
This will retrieve the value of the grade property if defined and store it in the const
variable. If this property is missing the const
variable will contain the string 'A'
.
Once an object has been created it can be modified in several ways.
- More object values can be added
- Object names can be deleted
- The values can be changed for existing names.
Once an object has been created, additional properties cane be added by setting values by assignment.
const employee = {
firstName: 'Colin',
'last name': 'Stephen',
'department': 'Computing'
}
employee.grade = 4
employee['currently employed'] = true
employee.department = 'Computer Science'
This sets a new value if the name does not already exist. Otherwise, it updates the existing value. Notice that the syntax depends on whether the property name is a valid JavaScript object and matches the syntax used to retrieve a property.
Properties can be removed from an object literal using the delete
operator. This removes the entire property (name and value).
const employee = {
firstName: 'Colin',
'last name': 'Stephen',
'department': 'Computing'
}
delete employee.department
Undefined Objects
If you try to retrieve a value from an object that is undefined, JS throws a TypeError exception:
const nonExistentObject.postCode // throws "TypeError"
const addressObject = employee.address // returns undefined
const postCode = employee.address.postCode // throws "TypeError"
To see what a typeError
looks like, try uncommenting the three lines at the end of the employee.js
file. So how can we avoid this?
The AND operator &&
can be used to guard against this problem.
const postCode = employee.address && employee.address.postCode
console.log(postCode) // returns undefined
Call by reference.
var a = {}
var b = {}
a.test = "hello"
b.test // returns undefined
var a = {};
var b = a;
a.test = "hello"
b.test // returns "hello"
Example.
var stooge = {first: "Jerome", second: "Howard"}
var x = stooge;
x.nickname = "Curly";
var nick = stooge.nickname;
nick // returns "Curly"
TODO
A constructor is any JavaScript function called with the new
keyword. Functions that are designed to be called by ‘new’ are called constructor functions.
A constructor function is a regular function except we use these with the new
keyword. There are a number of built-in constructor functions such as Array()
and Object
but we can create our own. To identify a constructor function their names should be capitalised.
Constructor functions are useful when you need multiple objects with the same properties and methods. Any objects created in this manner inherit all the properties and methods of the object prototype (see next section). Open the books.js
file to see an example of a constructor function and its invocation.
function Book(isbn, title) {
this.isbn = isbn
this.title = title
}
const b = new Book('1491943122', 'Learning Node')
Notice that the Book()
function is capitalised to identify it as a constructor function. When this code is run.
- it creates a brand new empty object called
b
. - calls the
Book()
function specified. - sets
this
to the new object - returns the new object
For any object we can use the instanceof
operator to check the constructor function used to create it.
if (b instanceof Book) console.log('its a Book')
Since functions are objects in JavaScript they get their own properties just like other objects. this
is a function-scoped variable that contains the value of the object that invoked the function. We use it to access the properties and methods of the object without needing to know the name of the invoking object. Adding properties and methods to this
is a safe way to store data that should be scoped to the function.
ife we look back at the previous example you will see that the two function parameters are added to the function's this
object as properties.
You have already seen properties in the built-in constructor functions. For example all Array
objects have a length
property. You can add properties to your own constructor functions as well.
function Book(isbn, title) {
this.isbn = isbn
this.title = title
this.year = null
Object.defineProperty(this, 'published', {
get: () => this.year,
set: year => this.year = year
})
}
const b = new Book('1491943122', 'Learning Node')
console.log(b.published) // prints null
b.year = 2016
console.log(b.published) // prints 2016
JavaScript has 8 built-in constructors, Object()
, String()
, Number()
, Boolean()
, Date()
, Array()
, Error()
and Regexp()
. All of these will allow you to create the appropriate objects however for each of these there is an alternative object literal that achieves the same result. For example to create an array or a string, these lines achieve the same result.
const arrLit = ['JavaScript', 'Swift', 'C++', 'Java']
const arrCon = new Array('JavaScript', 'Swift', 'C++', 'Java')
const strLit = 'Hello World'
const strCon = new String('Hello World')
If there are two ways to achieve the same result which one is preferred?
Generally, object literals are more concise and easier to read. They also run faster due to parse-time optimisations. For these reasons it is always recommended to create them using object literals.
TODO: Constructor example
As we have seen, functions are first class citizens, they can be used like any other data type. They can also be passed to other functions and returned from them. They can also be nested.
In JavaScript every function is actually a function object. Because of this we can use the new operator to create instances of them.
Open the coffee.js
file. Lets understand how it works.
- The
coffee.js
file contains a constructor calledCoffee()
that can be used to create new objects. - The function takes two parameters although the second one is a default parameter.
- Because we will be invoking the function as a constructor, the
this
object is bound to the returned object, this means that we will be able to access all its properties in our created object.- Notice that we added an object to
this
to store the different sizes and this is accessible by the function parameters.
- Notice that we added an object to
- We store the two parameters as properties of the
this
object. - We also store the
getSize()
functions in thethis
object which means we can call it once we have an instance of our enclosing function. Because we are storing the function as a function expression, we use the arrow function syntax. - We define a readonly property called
order
that will return a description of the drink order.
After the constructor is defined, we use this to create various coffee orders by passing different parameters to the constructor.
Run the script to see what it produces.
Notice that when we print one of our coffee objects we can access the local this
object meaning everything is public. This is not recommended and the next section describes how we can hide some of this.
- Modify the
.getsize()
property, replacing theswitch
with anif...else
block. - Now modify the
if...else
such that:- sizes up to 8 should be considered small.
- sizes between 9 and 12 should be medium.
- any size over 12 should be large.
- Add a third optional parameter called
shots
to allow the customer to specify additional coffee shots. The default value should be0
.- In the
order
property modify the message to include the number of additional shots. - Modify the message so that the coffee is labelled as
strong
if there are 2 or more additional shots.
- In the
The problem with the last example was that all the data was public. This is because it was assigned to the this
object which has visibility outside the function scope. To solve this we take advantage of the scoping of JavaScript functions using a special construct called a closure.
Open the betterCoffee.js
file which fixes this. Compare the code to the previous version and note:
We have moved the data we want to hide into a block scoped variable called privateData
, this prevents it being seen outside the function object.
const privateData = {}
privateData.size = {
small: 8,
medium: 12,
large: 16
}
The getSize()
method is not needed outside the function object and so it is defined locally.
function getSize() {
if (this.ounces === this.size.small) {
return 'small'
} else if (this.ounces === this.size.medium) {
return 'medium'
} else if (this.ounces === this.size.large) {
return 'large'
}
}
The order
property needs to be visible outside the function object. The only way to make it visible is to return it using the return
statement. By returning an object you can return many different methods.
Remember that we can assign a function to a variable (a function expression), all expressions return a value which, in the case of a function expression, is the function itself. Also remember that we can invoke a function by appending a pair pf parenthesis ()
.
Open the counter.js
file and see if you can understand what is happening. The purpose of an IIFE is to obtain data privacy.
TODO
JavaScript does not natively support classes, everything except primitives are objects. ECMAScript 6 introduces OOP behaviour using classes however behind the scenes this is just a thin syntactical rapper around a concept called prototypal inheritance, a topic we will be covering in this section.
As you already know, an object in JavaScript is a collection of key-value pairs. Each type of object (Strings, Arrays, etc.) has its own prototype which is an internal object from which any objects of that type share properties. What this means is that any objects inherit all the properties of the appropriate prototype object.
All built-in object types have their own prototype objects but so do any new objects that you define.
Each built-in object type has its own prototype object. This can be modified and any changes will affect any instance of that object.
Locate the prototypes/
directory and open the file palindrome.js
.
- Note how this module exports a single anonymous function.
- An anonymous function is declared and exported. For the shorter arrow function syntax see the link.
- this refers to the string that the prototype is called on.
- The function contains a loop that starts at each end of the string and compares the characters working inwards.
- If at any point these don't match, the function returns false.
- Notice that there is a block of code commented out which achieves the same result in 2 lines of code.
- Can you understand how this works?
Now open the index.js
file in the same directory.
- Run this script to view the output.
- The first line of code imports our module (the anonymous function) and assigns this to the String prototype.
- Next we create an array of the strings we want to test.
- JavaScript Array objects are iterable which means we can use a for-of loop to iterate through the indexes.
- The String.prototype.palindrome() function returns either true or false, we can then display the correct message.
- Notice that there is a block of code commented out which achieves the same result in 2 lines of code using the conditional operator.
- Can you understand how this works?
- Create a new file called
capitalise.js
. - Export a function that capitalises the first letter of the string
- Add a
capitalise
property to the String prototype. - Modify the test script in
index.js
to loop through the array of strings, capitalising the first letter of each. - Modify the prototype function to capitalise the first letter of each word.
Any functions you create will also have their own prototype
object. This allows you to change the behaviour of all instances of the object.
Locate the dog/
directory and open the file dog.js
.
- At the top of the script we define a
Dog()
function object that takes three parameters. Any values passed in the parameters are stored in thethis
object. - Next we modify the
Dog
function object, adding additional functionality through its prototype object. - Finally our module exports the
Dog
function object.
Now open the index.js
file in the same directory.
- we start by importing the Dog function object.
- Next we use the
new
keyword to create instances of our object, these will share the sameprototype
object. - The
spot
function object overides thebark()
method in the prototype.
- Add a new property to the prototype called
trained
. This should be added to theDog()
function object at the top as a fourth parameter with a default value oftrue
. - Modify the
sit()
prototype function so that the dog only sits iftrained
istrue
.
The latest version of JavaScript introduces the concept of classes in the traditional OOP sense. Behind the scenes however this is simply a lightweight syntax over the existing prototype-based inheritance. In JavaScript, classes are special functions and comprise two distinct components, class expressions and class declarations.
Open the classes/
directory and study the files it contains.
- The
person.js
file contains a class declaration.- the class is declared using the
class
keyword followed by the name of the class (its customary to start class names with an upper-case letter). - this class is exported by the module by assigning it to
module.exports
, which means it can be used in other scripts. - each class can have one and only one constructor.
- this constructor takes two parameters.
- each parameter is added to the
this
object which is private to the class and contains the private instance variables.
- the class is declared using the
- The
personTest.js
file contains a simple script, run it and watch the console output.- the module containing the class declaration is imported and stored in a constant.
- the code is wrapped in a try-catch block to handle any thrown exceptions
- the class is used to create a new Person object (called
person
) - two parameters are passed to the constructor.
- the
name
getter is called which computes the person's full name and returns it. - the
lastName
setter is called which updates the private instance variable. - finally the object is printed, this returns a JSON object containing the values stored in
this
. - the second object
badPerson
misses the second parameter from the constructor, notice that an exception is thrown.
- Modify the class declaration
- Modify the constructor to take a third parameter called
dob
. This should accept a JavaScript Date() object, this needs to be stored in a private instance variable calleddob
. - Modify the constructor to throw an exception if the third parameter is missing.
- Add a new getter to return the person's age in years.
- Test this functionality by modifying the test script
personTest.js
. Create a newDate()
object representing your date of birth and pass it to the constructor. - Use your new getter to print out the person's age.
- Modify the constructor to take a third parameter called
Open the employee.js
file and read through the code.
- The Employee class subclasses the Person class:
- The module containing the Person class needs to be imported at the top of our script.
- The
extends
keyword indicates that the Employee class subclasses the Person class.
- The third parameter passed to the constructor is a default parameter which means that if it is not specified, it is initialised with a default value.
- The constructor uses the
super()
keyword to call the constructor of its superclass, passing the appropriate parameters. - There is a method defined to calculate the salary.
- There is a single default parameter to determine the number of months salary to calculate.
- The calculation uses the salary grade passed to the constructor to calculate the result.
Open the employeeTest.js
file. This script is used to test our new class.
- Notice that we don't pass the
grade
parameter when we create theemployee
object, this assigns the default grade of 1. - When we create the
manager
object we pass a value for the grade parameter. - When we call the
calculateSalary()
method for theemployee
object we don't pass a parameter so the default value is used. - When we call the
calculateSalary()
method for themanager
object we pass a parameter.
Try running the employeeTest.js
script, notice that this doesn't work as expected, can you see why?
- In the previous section you changed the Person constructor, this change needs to be applied to the Employee constructor. Do this now then check to see it works correctly.
- Create a new subclass of
Person
calledPassenger
in a new file calledpassenger.js
- Create an optional parameter in the constructor called
airMiles
with a default value of0
- Create a method called
addAirMiles()
, this should take an integer and add this to the current miles. - Create a getter that returns the current number of air miles.
Using the Reflect
object.