Skip to content

Latest commit

 

History

History
379 lines (281 loc) · 18.8 KB

javascript-7-oop.md

File metadata and controls

379 lines (281 loc) · 18.8 KB

JavaScript 7 - Object-Oriented Programming

Projected Time

3 hours total

40 min - Slideshow

10 min - Video

30 min - Lesson Text

40 min - Independent Practice

Prerequisites

Motivation

  • Principles of object-oriented programming (aka "OOP") help developers to organize their code. It's really a way of thinking, about how code and data can interact, and how to write code to make it more reusable. It allows developers to organize code to put similar objects under the same category (called "classes"), in order to share code and logic. It also allows other parts of the code to know how to utilize objects, by knowing what's available from that class.

Virtually all companies will use object-oriented programming in their code. For example, when you do a Yelp search for "burgers", each area on the screen that describes each recommended restaurant is an instance of a class. The information is probably abstracted into a class of "Restaurant", of which "Marlowe" and "Super Duper Burgers" are instances of that class.

Objectives

Participants will be able to:

  • Understand some of the main concepts of object-oriented programming
  • Start developing an understanding of objects and classes

Specific Things to Learn

  • Classes and objects
  • OOP (Object-Oriented Programming)
  • Inheritance
  • Polymorphism
  • Overriding

Materials

Lesson

Read through the OOP slideshow, then watch this OOP video before moving onto the lesson with code examples below. There are a lot of new concepts, so don't forget to take breaks!

Refresher: What is an Object?

If you recall from JavaScript 6 - Object Literals-

An object is a thing that has properties. This sounds simple, but it's actually very abstract! To help flesh this out, think of an example software application that keeps track of books, such as for a library. In this application, a book can be thought of as an object that has certain properties like title and author.

For example:

const book = {
  "id": "827392838",
  "authorFirstName": "Jane",
  "authorLastName": "Doe",
  "title": "The Wonderful World of JavaScript",
};

In the same example software application, we might also want to keep track of people who will borrow library books:

const borrower = {
  "id": "9002",
  "firstName": "Syma",
  "middleInitial": "N",
  "lastName": "Tec",
  "phoneNumber": "(415) 123-1234",
};

Properties on an object are sometimes also referred to as "key-value pairs." Every property has a name (aka the key), "firstName", and a value, "Jane".

What is a Class?

You can think of a class as a blueprint, or template, for creating an object. In the example above, the book object could be created from this class:

class Book {
  constructor(id, title, authorFirstName, authorLastName) {
    this.id = id;
    this.title = title;
    this.authorFirstName = authorFirstName;
    this.authorLastName = authorLastName;
  }
  summary() {
    return this.title + " (" + this.authorLastName + ", " + this.authorFirstName + ")";
  }
}

And the Borrower class could look like:

class Borrower {
  constructor(id, firstName, middleInitial, lastName, phoneNumber) {
    this.id = id;
    this.firstName = firstName;
    this.middleInitial = middleInitial;
    this.lastName = lastName;
    this.phoneNumber = phoneNumber;
  }
}

A class is the set of properties that describe an item, properties that make sense belonging all together in a single object. In the example above, all the information about a book is contained in the book object and all the information about a borrower is contained in the borrower object. Properties about a book are not stored inside a borrower object, and vice versa.

Methods (aka functions) can be part of objects too, you can think of them as functions that are attached (or bound) to the object. For example, a method called summary() can return a one-line summary about a book. Or, a method called checkOut() would make sense on a borrower object. It would not be relevant for a borrower object to have a method summary(), because the borrower object does not contain any text that needs to be summarized.

This process of deciding what properties and methods belong to a particular class is sometimes called "abstraction" and is all part of the object-oriented programming experience. It is very similar to the thought process by which we decide the columns that go into a database table. A table called books would probably have very similar columns as the properties found in the Book class.

Creating objects from a class

When you instantiate an object from a class, using the "new" keyword, that means you are creating a new object, using the class as a template, like this:

let myBook = new Book(55234, "Principles of OO Design", "Barbara", "Liskov");

Notice the function named "constructor" in the class definition, above? That is the function which is called when you use the "new" keyword with a class name. The values that you pass in parentheses become available as the named parameters list in the constructor function (id, title, authorFirstName, authorLastName).

After instantiating the Book as myBook, you can inspect it from the console-

> console.log(myBook.authorLastName);
'Liskov'

The this keyword

this is a special keyword in JavaScript which refers to the current object instance. When you create an object with the new keyword, the code in your methods (functions) can refer to properties on the current instance using this. In the example above, in the Book class, the constructor method uses this to refer to the object being constructed (via new) and the summary method refers to the object that the code is currently working with.

Why this matters: you may have an many Book objects, and want to print the summary for each one-

> const books = [];
> books[0] = new Book(12345, "Why Didn't They Ask Evans?", "Agatha", "Christie");
> books[1] = new Book(12346, "The Long Goodbye", "Raymond", "Chandler");
> books[2] = new Book(12347, "Decline and Fall", "Evelyn", "Waugh");

Now, test the result-

> console.log(books[0].summary());
'Why Didn\'t They Ask Evans? (Christie, Agatha)'
> for(let i=0; i<3; i++) {
...   console.log(books[i].summary());
... }

Because the books array contains three Book definitions, and we asked to refer to just one (books[0]), inside the .summary() method the this keyword "knows" to point to the correct instance of the Book class.

Concepts of Object-Oriented Programming

Encapsulation

Encapsulation is the practice of keeping all of the things necessary to interact with a class in a single place. For instance, by providing a set of methods on an object, you are exposing the actions that somebody might take on it. This will make it easier for others to use your code and give you the ability to prevent others from misuse.

Encapsulation means that all the actions that we might take out on an object exist in one place. For instance, if a Borrower wanted to check out a book, we could use a checkOut method on that object to perform the action. In this example, each Borrower has an array of books they have checked out, and the checkOut function adds a new book to that list. As a user of this class, all we need to know is that this method takes a book and then the method will perform the necessary actions to check out our book. The actual implementation details are "encapsulated" within the Borrower class.

class Borrower {
  constructor(id, firstName, middleInitial, lastName, phoneNumber) {
    this.id = id;
    this.firstName = firstName;
    this.middleInitial = middleInitial;
    this.lastName = lastName;
    this.phoneNumber = phoneNumber;
    this.libraryBooks = []; // list of books checked out from library
  }

  checkOut(book) {
    this.libraryBooks.push(book);
  }
}
Abstraction

As stated above, because the checkOut() method is encapsulated in the borrower class, the rest of the program can just call the checkOut() method and know it will perform the necessary actions to check out our book without needing to know anything about the function.

let MaryCrowley = new Borrower("1234567", "Mary", "E", "Crowley", "(555)123-4567");
MaryCrowley.checkOut(myBook);

Any new instance of the borrower class will have a checkOut() method, too. Since checkOut() has been abstracted away from the rest of the program by encapsulating it in the Borrower class, just calling MaryCrowley.checkOut(myBook) will take care of it all using the specific details of that Borrower.

Another example is an iPhone. There's a whole lot going on inside, but all of the complexity of the actions is abstracted away so all the user has to understand is how to use the buttons and touchscreen interface to, say, call an openTheApp() method.

Inheritance

Inheritance is a way for objects to inherit properties and methods from other objects.

For example, let's say that our library doesn't just have books, but they also have audiobooks on CD to loan out. It's possible we can create an AudioBook class, and this will have properties like title, authorFirstName and authorLastName just like the Book class:

class AudioBook {
  constructor(id, title, authorFirstName, authorLastName) {
    this.id = id;
    this.title = title;
    this.authorFirstName = authorFirstName;
    this.authorLastName = authorLastName;
    this.lengthInMinutes = 90;
  }
}

You'll see that the AudioBook class is exactly like the Book class but with a lengthInMinutes property added. Rather than constructing it this way, we can reuse code in the Book class instead by making AudioBook a child, or subclass, or Book:

class AudioBook extends Book {
  constructor(id, title, authorFirstName, authorLastName, lengthInMinutes) {
    super(id, title, authorFirstName, authorLastName);
    this.lengthInMinutes = 90;
  }
}

let myAudioBook = new AudioBook(55234, "Principles of OO Design", "Barbara", "Liskov", 206);

The relationship between AudioBook as the parent class and Book as the child class comprises a class hierarchy. A class hierarchy can go on for multiple levels of parents and children, and parents can have many children. For example the library might have e-books--an ElectronicBook class which is also a child of Book, making it a sibling of AudioBook.

And what if our library has special items, like telescopes, to lend out? We might want to add a parent above Book, called LoanItem. Telescope would be a subclass of LoanItem, at the same level as Book.

The entire class hierarchy would then look like this:

       LoanItem
         /\
        /  \
       /    \
      /      \
Telescope    Book
              /\
             /  \
            /    \
           /      \
     AudioBook   ElectronicBook
Overriding

Overriding happens when a subclass changes a value or behavior inherited from its parent class. For example, let's say that in our library application, every object that can be borrowed has a renewalLimit property, which is the maximum amount of times one borrower can renew a loan.

The Book class might look like this:

class Book {
  constructor(id, title, authorFirstName, authorLastName) {
    this.id = id;
    this.title = title;
    this.authorFirstName = authorFirstName;
    this.authorLastName = authorLastName;
    this.renewalLimit = 5;
  }
}

Let's say that audiobooks, however, are in high demand and in short supply. So, the library might only want to let borrowers renew an audiobook loan once. In this case, the AudioBook class, which still extends (inherits from) the Book class, can look like this:

class AudioBook extends Book {
  constructor(id, title, authorFirstName, authorLastName, lengthInMinutes) {
    super(id, title, authorFirstName, authorLastName);
    this.lengthInMinutes = 90;
    this.renewalLimit = 1;
  }
}

Here the AudioBook class is overriding the renewalLimit property that it inherited from Book. While Book objects have a renewalLimit value of 5, AudioBook objects have a renewalLimit of just 1.

Polymorphism

Polymorphism is the ability to get different behaviors from objects even when the same method is being called. When we use polymorphism, we may not even know the subclass of the object we are accessing. This is because of inheritance: an object created from a subclass will have all the properties and methods inherited from any parent class in the hierarchy. And as long as we use a property or method that exists in a parent class, we don't need to actually know the subclass of the object.

To explore this, let's say that our Borrower class has a method called checkOut(). It takes a single argument, which is an object of type Book or any subclass of Book. The purpose of this method is to check out a book from the library and return the due date for returning the book to the library.

class Borrower {
  constructor(id, firstName, middleInitial, lastName, phoneNumber) {
    this.id = id;
    this.firstName = firstName;
    this.middleInitial = middleInitial;
    this.lastName = lastName;
    this.phoneNumber = phoneNumber;
    this.libraryBooks = []; // list of books checked out from library
  }

  checkOut(book) {
    this.libraryBooks.push(book);
    const borrowedDate = new Date();
    const dueDate = book.calculateDueDate(borrowedDate);
    return dueDate;
  }
}

Notice that the dueDate variable is populated by a method called calculateDueDate() which belongs to the book object. The Book class and its extension, AudioBook, both implement this method. But let's say the calculation is different, because a book can be lent out for 3 weeks while an audiobook can be lent out for 2 weeks.

class Book {
  ...
  
  calculateDueDate(borrowedDate) {
    return borrowedDate.setDate(borrowedDate.getDate() + 21);  // 21 days is 3 weeks
  }
}

class AudioBook extends Book {
  ...
  
  calculateDueDate(borrowedDate) {
    return borrowedDate.setDate(borrowedDate.getDate() + 14);  // 14 days is 2 weeks
  }
}

In this case, when the checkout function is called, the checkout function has no idea what subclass is being passed in. It could be an object from the Book class, or it could be an object from the AudioBook class. Yet it knows that whatever object is passed in has a calculateDueDate() method, because all subclasses of Book will either inherit that method as is, or override it, as in the case of AudioBook.

The checkout function is using the calculateDueDate() method polymorphically. It's the same method name, regardless of the subclass. But the behavior differs: in the case of a Book object, the value returned will be 3 weeks from borrowedDate; in the case of an AudioBook object, the value return will be 2 weeks from borrowedDate.

Multiple ways to create objects in JS

There are multiple ways to create objects in JavaScript. As you do different tutorials, you might come across different syntax for creating objects. The good thing is that the concepts used are the same no matter which syntax you use to define the object.

Read this article to learn about 4 different ways to define an object in JS: https://dzone.com/articles/easy-javascript-part-13-four-ways-to-create-object

Independent Practice

Exercise 1

Exercise 2

Copy the Book, AudioBook, and Borrower class code from the lesson above into a file on your computer. Write code to do the following:

  1. Add a function on Borrower called getLastBook() that returns the last book they borrowed
  2. Create at least 2 instances of Borrower, 2 instances of regular Book, and 2 instances of AudioBook
  3. Make one borrower check out 3 books
  4. Log the Books the person borrowed
  5. Log the last Book the person borrowed

For an extra challenge, add the following functions to the Borrower class and test them:

  1. favoriteAuthor() returns the author that the Borrower has borrowed the most books from
  2. returnBook(bookId) removes the book with the given ID from the Borrower's list of books

Check for Understanding

  • Explain your completed Book and Borrower code to a partner.

Challenge

There are some more advanced concepts related to OOP you can learn. Find definition of following terms on your own:

  • Namespace
  • Hoisting
  • Arity or function.prototype.length
  • Anonymous
  • Closure
  • Currying
  • Pragma

Supplemental Resources