Types and Objects

This chapter and the next will give you an overall taste of Object-Oriented Programming in JavaScript. This book will cover all these topics.

Object-Oriented Programming, often referred to as OOP, is a programming paradigm created to deal with the growing complexity of large software systems. Programmers discovered early on that as applications grew in complexity and size, they became difficult to maintain. One small change at any point in the program could trigger a ripple effect of errors due to dependencies elsewhere.

Programmers needed a way to create containers for data that could be changed and manipulated without affecting the entire program. They also needed a way to section off areas of code that performed specific procedures so that their programs could become the interaction of many small parts rather than one massive blob of dependency.

Enter OOP. Let's first introduce some basic concepts and terminology.

Objects

Objects form the basis for JavaScript's type system. Other than a handful of primitive types, such as numbers, strings, and booleans, almost all types in JavaScript are based on objects. Even the primitive types can be represented as objects, though this is generally invisible to the programmer.

While objects represent individual things - my cat Cheddar, your red convertible, Chris's basketball - it's helpful to think of these objects as having a type. Cheddar is a specific type of cat, your convertible is a particular type of automobile, and the basketball is a specific type of sports equipment. You can have multiple instances of any of these types. Each instance is an object with all the data and behaviors of the given type. For example, we can have multiple cats, automobiles, or items of sports equipment.

We will usually use the term "type" when referring to a primitive value or object; we'll use "class" when referring to an object whose type was defined with the class keyword, which we'll encounter later. Outside of Launch School, you will likely see much variation. JavaScript programmers often don't make huge distinctions about type vs. class.

Objects are also called instances or instance objects. They are often created by a constructor function, a concept we'll explore in detail later. For now, here's an example of creating two instance objects with a constructor function:

function Cat(name) {
  this.name = name;
}

let butterscotch = new Cat("Butterscotch");
let pudding = new Cat("Pudding");

Calling the Cat function with the new keyword makes the call a constructor call. We also refer to Cat as a constructor function. The new keyword performs several actions and returns an object whose type is the same as the function's name. Thus, butterscotch and pudding are instances of the Cat type.

You can also define methods in a constructor function:

function Cat(name) {
  this.name = name;

  this.purr = function() {
    console.log('purr');
  }

  this.speak = function() {
    console.log('meow!');
  }
}

let butterscotch = new Cat("Butterscotch");
butterscotch.purr();          // purr
butterscotch.speak();         // meow!

let pudding = new Cat("Pudding");
pudding.purr();               // purr
pudding.speak();              // meow!

Instantiation is the process of creating a new instance object. It's a fancy word for a fundamentally simple idea, but it makes you sound smart. Okay, that's not the real reason we use the term; instantiation is more specific than creation. However, in JavaScript, there is little difference between the two.

We typically use nouns when discussing types and objects; they represent concrete (non-abstract) things. Many, but not all, methods are named with verbs; they represent actions and behaviors. The objects we create are specific instances of the different types we define. The objects are our nouns, and we manipulate them using the verbs provided by the methods available to those objects.

Classes and Types

In most OO languages, programmers define reusable classes as blueprints for objects. Each class describes the state and behaviors of objects of that particular class.

JavaScript, however, is not a traditional OO language. Technically, it doesn't support classes in the conventional sense. Even though it has something it calls classes, those classes are merely syntactical sugar for JavaScript's type system. JavaScript classes are relatively new, having been introduced to JavaScript in ECMAScript 2015 (a.k.a. ES6).

We will refer to true classes as true classes, conventional classes, or traditional classes to distinguish them from JavaScript's syntactic classes. The three terms are synonymous.

Talking about classes in JavaScript can lead to ambiguity. For instance, we can define types in various ways, only one of which uses the class syntax. We will try to reserve the term class when talking about types defined by the class syntax. We'll use the term types instead of classes when speaking more generally.

As mentioned earlier, traditional classes are blueprints for objects. With JavaScript, the non-primitive types are similar: they are blueprints for objects.

Creating Objects

In our Introduction to Programming with JavaScript book, we introduced you to creating objects in JavaScript. At its most basic, you can create objects using object literals. For instance, here's how we create a student object with two properties: a name property with a value of Joanna, and an age property of 27:

let student = {
  name: 'Joanna',
  age: 27,
};

This code creates a simple data structure to store some data about a student. However, OOP is about combining data and behavior into an object. Here, we only have data. If we want to also provide some behavior for the student, we need to create some methods in the object. Methods are functions carried around by objects. When called, methods access and manipulate the associated data. For instance:

let student = {
  name: 'Joanna',
  age: 27,

  //highlight
  study: function() {
    console.log(`${student.name} is studying`);
  },

  pass: function() {
    console.log(`${student.name} has passed this course`)
  },
  //endhighlight
};

//highlight
student.study();  // Joanna is studying
student.pass();   // Joanna has passed this course
//endhighlight

This code bundles the data and operations related to a student into an object. The study and pass properties define two methods: one prints Joanna is studying and the other prints Joanna has passed this course.

The object's structure is essentially the same as the objects we've encountered so far. The chief difference is that some of the property values are functions. That shouldn't be surprising; we've seen before that JavaScript functions are first-class values, meaning we can treat them like any other JavaScript value. That includes using them as object property values. When object properties have function values, we call them methods.

The use of student.name in the functions is required to access the value of the name property. We would also need student.age to access the age property. We'll see a better way to deal with this issue soon.

One advantage of this approach is clear: if we want to operate on the student, we don't have to search for the function and data we need. We can see at a glance what you can do with the student merely by looking at the object.

We also use dot notation to call a method. For instance:

student.study();

Concise Method Syntax

Using methods is so common that there's a shorthand syntax called the concise (or compact) method syntax for it:

let student = {
  name: 'Joanna',
  age: 27,

  //highlight
  study() {
  //endhighlight
    console.log(`${student.name} is studying`);
  },

  //highlight
  pass() {
  //endhighlight
    console.log(`${student.name} has passed this course`)
  },
};

student.study();  // Joanna is studying
student.pass();   // Joanna has passed this course

You can omit the : and the function keyword and use parentheses to denote a method.

The this Keyword

Thus far, in our example, we refer to the object from inside the methods by directly using the variable name, student. This could be better. If we have multiple students with names like student1, student2, etc., we must change each method to use a different name. We need some way to refer to the object that contains a method from other methods in that object. The keyword this provides this functionality:

let student = {
  name: 'Joanna',
  age: 27,

  study() {
    //highlight
    console.log(`${this.name} is studying`);
    //endhighlight
  },

  pass() {
    //highlight
    console.log(`${this.name} has passed this course`)
    //endhighlight
  },
};

student.study();  // Joanna is studying
student.pass();   // Joanna has passed this course

Thus, if we want a different student, we can copy and paste everything:

//highlight
let anotherStudent = {
  name: 'Kim',
  age: 30,
//endhighlight

  study() {
    console.log(`${this.name} is studying`);
  },

  pass() {
    console.log(`${this.name} has passed this course`)
  },
};

//highlight
anotherStudent.study();  // Kim is studying
anotherStudent.pass();   // Kim has passed this course
//endhighlight

The workings of this are one of the most difficult JavaScript concepts to grasp; it's the source of a great deal of confusion. We'll discuss it later in this book and even more extensively later in Core. For now, you can assume that when you use this inside a method, it refers to the object that contains the method.

State and Behavior

As we mentioned earlier, we use types to create objects. We typically focus on state and behavior when defining a type. State refers to the data associated with an individual object, which is tracked by instance properties, i.e., properties that belong to an instance object. Behavior encompasses the actions that an object can perform.

For example, let's create some objects to represent cats. We won't follow this pattern too often, but we're not yet ready to look at any of the standard object creation patterns:

let Cat = {
  purr() {
    console.log("Purr...");
  },

  eat() {
    console.log("I am eating...");
  },
};

let cheddar = Object.assign({}, Cat);
let cocoa = Object.assign({}, Cat);

cheddar.name = 'Cheddar';
cheddar.color = 'orange';
cheddar.gender = 'male';

cocoa.name = 'Cocoa';
cocoa.color = 'black';
cocoa.gender = 'female';

In this snippet, we've started by creating an object named Cat that defines a new object type in the program: a Cat type. The Cat type has two methods, purr and eat.

After defining the Cat type, we create two objects using the Cat object to provide their methods. We use Object.assign in this code to create the new objects. Object.assign({}, Cat) copies all of the methods and properties defined by the Cat object to an empty object ({}), then returns the resulting object. Thus, cheddar and cocoa acquire purr and eat instance methods from the Cat object.

Finally, we define the states for the cheddar and cocoa objects by assigning values to the name, color, and gender instance properties of cheddar and cocoa.

Even though cheddar and cocoa reference different objects, both contain identical behaviors as defined by the purr and eat instance methods. Both cats can purr and eat. We've described these behaviors as instance methods in the Cat object. Such instance methods are available to all objects (or instances) created by that type.

In summary, instance properties track state, and instance methods expose behavior for objects.

Summary

In this chapter, we've learned about objects and types in JavaScript. We've also discussed instantiation, behavior, and state. Now, it's time to learn the basic concepts behind object-oriented programming.

Exercises

  1. Create an object that represents a Cessna 152 aircraft. The aircraft should include information that shows its fuel capacity of 24.5 gallons and a cruising speed of 111 knots. The aircraft should be able to take off and land.

    Identify the state and behavior items in this object.

    Solution

    let cessna152 = {
      fuelCapacityInGallons: 24.5,
      cruisingSpeedInKnots: 111,
    
      takeOff() {
        console.log("Taking off!");
      },
    
      land() {
        console.log("Landing!");
      },
    };
    
    cessna152.takeOff();       // Taking off!
    cessna152.land();          // Landing
    

    Note that we've used the concise method syntax above for the takeOff and land methods. We could also use the traditional method syntax:

    let cessna152 = {
      fuelCapacityInGallons: 24.5,
      cruisingSpeedInKnots: 111,
    
      //highlight
      takeOff: function() {
      //endhighlight
        console.log("Taking off!");
      },
    
      //highlight
      land: function() {
      //endhighlight
        console.log("Landing!");
      },
    };
    

    The state includes the fuelCapacityInGallons and cruisingSpeedInKnots properties, while the behavior includes the takeOff and land methods.

  2. Write a simple constructor function that creates objects that represent books. Each book should have a title, author, and the year published. Create objects that represent the following books:

    Title Author Year Published
    Neuromancer William Gibson 1984
    Doomsday Book Connie Willis 1992

    When you are done, identify the type of the objects created, the constructor function, and the instance objects.

    Solution

    function Book(title, author, published) {
      this.title = title;
      this.author = author;
      this.published = published;
    }
    
    let neuromancer = new Book('Neuromancer', 'William Gibson', 1984);
    let doomsday = new Book('Doomsday Book', 'Connie Willis', 1992);
    

    The object type is Book, the constructor is the Book function, and the instance objects are the objects assigned to neuromancer and doomsday.

  3. Write a simple constructor function that creates objects that represent musical albums. Each album should have a title, artist, and release year. Create objects that represent the following 2 albums:

    Title Artist Release Year
    Thriller Michael Jackson 1982
    The Dark Side of the Moon Pink Floyd 1973

    When you are done, identify the type of the objects created, the constructor function, and the instance objects.

    Solution

    function Album(title, artist, releaseYear) {
      this.title = title;
      this.artist = artist;
      this.releaseYear = releaseYear;
    }
    
    let thriller = new Album('Thriller', 'Michael Jackson', 1982);
    let darkSide = new Album('The Dark Side of the Moon', 'Pink Floyd', 1973);
    
  4. Write a constructor function that creates objects that represent smartphones. Each smartphones should have a brand, model, and release year. Add methods to check the battery level and to display the smartphones's information. Create objects that represent the following two smartphones:

    Brand Model Release Year
    Apple iPhone 12 2020
    Samsung Galaxy S21 2021

    Solution

    function Smartphone(brand, model, releaseYear) {
      this.brand = brand;
      this.model = model;
      this.releaseYear = releaseYear;
    
      this.checkBatteryLevel = function() {
        return `${this.brand} ${this.model} has 75% battery remaining.`;
      };
    
      this.displayInfo = function() {
        return `${this.releaseYear} ${this.brand} ${this.model}`;
      };
    }
    
    let iphone12 = new Smartphone('Apple', 'iPhone 12', 2020);
    let galaxyS21 = new Smartphone('Samsung', 'Galaxy S21', 2021);
    
    console.log(iphone12.checkBatteryLevel());
    // Apple iPhone 12 has 75% battery remaining.
    
    console.log(iphone12.displayInfo());
    // 2020 Apple iPhone 12
    
    console.log(galaxyS21.checkBatteryLevel());
    // Samsung Galaxy S21 has 75% battery remaining.
    
    console.log(galaxyS21.displayInfo());
    // 2021 Samsung Galaxy S21