Step 4: JavaScript Objects

April 30, 2021

Up until this point, we have learned about data types such as numbers, strings, and booleans. We have also learned about a couple of different other built-in values like undefined and null. It is time to learn a new data type called objects.

An object in Javascript is a data type that helps to organize other data together. We can create an object by using curly braces.

const x = {};

This code above creates an empty object. We can now populate this object with values. A value is stored as a property of the object. Say, we want this object to have a property called name that will have the value John. Here is how we do that:

const x = {};
x.name = "John";
console.log(x);

This way of working with the properties on an object using a dot (.) is called dot notation.

If we are to console.log this object, we would see that it has a property called name that has the value "John". Let's add another property to this object called lastName.

x.lastName = "Doe";

As we can see, we can keep adding new properties and values to this object. This object that we are creating represents data about a person given it has properties called name and lastName. Since this object represents a person, we should probably find a better variable name than x. Let's rewrite our example with a proper variable and property names.

const person = {
  firstName: "John",
  lastName: "Doe",
};

This rewrite is much better. As we can see in this example, we can also populate an object at initialization.

We can use the dot notation to access the values that are stored on objects. If we wanted to get the value of firstName on the person object. We can do this:

console.log(person.firstName);

And this would display the value of the firstName property.

We can use the dot notation to assign a different value to a property as well.

person.firstName = "Jane";

We can also have functions as properties on an object. Here is how to do it.

function hello() {
  console.log("Hello");
}

const person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: hello,
};

person.sayHello();

Alternatively we can define the function property at object initialization as well.

const person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: function hello() {
    console.log("Hello");
  },
};

person.sayHello();

Function properties of objects are called methods. sayHello is a method of the person object. An object is made up of properties and methods.

Now that we have learned about objects and the dot notation, the dot symbol inside the console.log function should make more sense. log is a method of the console object. If we are to console.log the console object, we would see that it has many other methods.

console.log(console);

When defining object properties, we can use the shorthand notation if we have a variable with the same name as an object property. Here is how it works:

const firstName = "John";
const lastName = "Doe";

const person = {
  firstName,
  lastName,
  sayHello: function hello() {
    console.log("Hello");
  },
};

person.sayHello();

We have used the shorthand notation when defining the firstName and the lastName. We simply wrote the variable name, and the object accepted it as the value since it has the same name as the property name. This saves us some time when we are defining objects.

We can delete a property from an object by using the delete keyword.

delete person.lastName;
console.log(person);

An object can store any value, even other objects!

const person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: function hello() {
    console.log("Hello");
  },
  age: 42,
  isAlive: true,
  workInformation: {
    companyName: "NASA",
    position: "Software Engineer",
  },
  children: null,
};

console.log(person);

We can also use square brackets ([]) to work with the properties and methods of objects. Here is how it works.

const person = {};
person["firstName"] = "John";
console.log(person["firstName"]);

The dot notation is easier to use but square brackets has some advantages over the dot notation.

Using square brackets, we can have numbers as property and method names. This is not possible to do while using dot notation.

const x = {};
x[42] = "Answer to everything";
console.log(x[42]);

Using square brackets we can also use variables as object keys.

const x = {};
const key = 42;
x[key] = "Answer to everything";
console.log(x[key]);

This is a very useful feature to have when we are programmatically creating an object.

this Keyword

Let's take a look at this example again, where calling the sayHello method on the person object displays the word Hello to the screen.

const person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: function hello() {
    console.log("Hello");
  },
};

person.sayHello();

What if we wanted to have the sayHello method to make use of a stored value on the person object? For example, wouldn't it be great if calling the sayHello method on this person object displayed Hello, my name is John Doe to the screen? We could try to solve this issue by hardcoding the firstName and lastName values inside the sayHello function.

{
  const person = {
    firstName: "John",
    lastName: "Doe",
    sayHello: function hello() {
      console.log(`Hello, my name is John Doe`);
    },
  };

  person.sayHello();
}

This wouldn't work great since we might want to update the values for firstName or the lastName properties later, but then the sayHello wouldn't get updated with this change.

person.firstName = "Jane";
person.sayHello(); // would display `Hello, my name is John Doe`

We can address this issue by making use of the this keyword. this keyword allows the object to refer to itself.

const person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: function hello() {
    console.log(`Hello, my name is ${this.firstName} ${this.lastName}`);
  },
};

person.sayHello();

person.firstName = "Jane";
person.sayHello();

As we can see in this example, this.firstName refers to the firstName property that is on the person object.

We have been assigning a function with the name hello to the sayHello method, but that function does not actually have to have a name since it will be accessed through the method name. We could simply use an anonymous function in there.

const person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: function () {
    console.log(`Hello, my name is ${this.firstName} ${this.lastName}`);
  },
};

person.sayHello();

person.firstName = "Jane";
person.sayHello();

But keep in mind that we shouldn't use an arrow function when defining methods. Arrow functions have a different behavior around the this keyword, making them unsuitable to be used in this context. Take a look at the result we get when we use an arrow function instead of a normal function definition for the sayHello method.

const person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: () => {
    console.log(`Hello, my name is ${this.firstName} ${this.lastName}`);
  },
};

person.sayHello(); // Hello, my name is undefined undefined

person.firstName = "Jane";
person.sayHello(); // Hello, my name is undefined undefined

this keyword does not refer to the object itself anymore when used inside an arrow function.

Objects help us with code organization. We now have an object representing a concept (a person) with all the properties and methods related to that object in a single spot. An alternative to this approach would have looked like this:

const firstName = "John";
const lastName = "Doe";

function sayHello(firstName, lastName) {
  console.log(`Hello, my name is ${firstName} ${lastName}`);
}

sayHello(firstName, lastName);

This code does the same thing but it feels a bit less organized. sayHello function is clearly built to be used with a first name and a last name but it is not structurally tied to those concepts. Objects help us to resolve that issue.

Passed by Value vs. Passed by Reference

An important concept about objects is regarding how they are stored in the computer memory.

All the data types that we have learned before this section are passed by value. This means that a new copy of the value is created when they are assigned into a variable.

let x = 42;
let y;

y = x;
x = 7;

console.log(x);
console.log(y);

What do you think will get displayed on the screen? If you have guessed 7 and 42, you are correct.

Let's take a look at a similar example with objects to see what happens in that case.

let x = { number: 42 };
let y;

y = x;
x.number = 7;

console.log(x);
console.log(y);

We would see that both x and y display the same value when we console.log.

Objects are passed by reference. When we assign the value of x to the variable y, we don't create a new copy of x. x and y start referring to the same object in the computer memory. This is why the value of the variable y changes when we change the number property of x.

This is a critical behavior to keep in mind. Thinking that we are creating an entirely new copy of an object while we are just creating a reference is a common source of bugs.

There are cases when we might want to create a copy of an object. We can use the spread operator to do that. Spread operator (...) allows us to copy an object to a new value.

let x = { number: 42 };
let y;

y = { ...x };
x.number = 7;

console.log(x);
console.log(y);

We still need to be careful, though, because creating a copy using the spread operator would not create a copy of the deeper objects that might be nested in the object.

const person = {
  firstName: "John",
  lastName: "Doe",
  sayHello: function hello() {
    console.log("Hello");
  },
  age: 42,
  isAlive: true,
  workInformation: {
    companyName: "NASA",
    position: "Software Engineer",
  },
  children: null,
};

const anotherPerson = { ...person };

anotherPerson.firstName = "Jane";
anotherPerson.workInformation.companyName = "SpaceX";

console.log(person.firstName);
console.log(person.workInformation.companyName);

In this example, we have a person object. We are then creating a copy of the person object by using the spread operator. This copy has a different name and works at a different company. Then we are checking to see what happened with the original person data. The person's firstName is still the same, but the workInformation had mutated when we changed it for anotherPerson. That is because the spread operation doesn't create a deep copy. It only creates a shallow copy.

If we wanted to create a deep copy or a deep clone of a JavaScript object, then we might want to use external programming libraries created for that purpose. We will learn about programming libraries at a later time.

Factory Functions, Constructor Functions, and Classes

In the previous examples, we have been creating an object to hold values that describe a person. Having data structures that represent some real or abstract concept is very common in programming. A financial application might have to represent the concept of money, a transaction, or a wallet. An e-commerce application can have representations for a cart, store, or product. An HR application might have to represent the concept of a person, just like we are doing here.

As we have seen in the example, we could easily create an object that holds the data representing a person. We had some issues when we wanted to create another object for a different person. If we wanted to create many instances of an object easily, we need to learn about factory functions, constructor functions, and classes. They are all different ways of creating an object template that can be used to create multiple instances of an object.

Factory Functions

A factory function is essentially a function that returns an object. We can use such a function as a way to create multiple instances of an object. Here is an example:

function getPerson(firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello: function () {
      console.log(`Hello, my name is ${this.firstName} ${this.lastName}`);
    },
  };
}

const personA = getPerson("John", "Doe");
const personB = getPerson("Jane", "Doe");

personA.sayHello();
personB.sayHello();

Here, getPerson is a factory function since it allows us to create multiple instances of an object. There is nothing really special about a factory function. It is just a simple function that provides us with a way to create multiple instances of the same object.

There are some other methods for creating an object template. We won't be using them in this book since factory functions are good enough for our use cases. However, it is still useful to learn about them since we might come across their usage when looking at different codebases. Those other methods are constructor functions and classes.

Constructor Functions

A constructor function is a function that acts as a template for creating multiple instances of a same object. Let's take a look at an example:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.sayHello = function () {
    console.log(`Hello, my name is ${this.firstName} ${this.lastName}`);
  };
}

const personA = new Person("John", "Doe");
const personB = new Person("Jane", "Doe");

personA.sayHello();
personB.sayHello();

In this example, Person is a constructor function. It defines a template for a person object. We then create instances of that object by calling this constructor function with the new keyword. A constructor function is just like a normal function but there are a couple of things that set it apart.

Constructor functions are defined with a title case name (Person instead of person). This is a hint that this function is a constructor function and should be called with the new keyword. If we don't call a constructor function with the new keyword, this keyword won't point to the object we have created. It simply won't work as expected.

I have rarely had to define any constructor functions, but they are good to know because we might have to use constructor functions defined by others. We have to remember to call them with the new keyword.

Classes

Another way of creating an object template in JavaScript is by using classes. A class is a special function that provides a different syntax for defining object templates. Here is how the above example can be written as a class.

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  sayHello = function () {
    console.log(`Hello, my name is ${this.firstName} ${this.lastName}`);
  };
}

const personA = new Person("John", "Doe");
const personB = new Person("Jane", "Doe");

personA.sayHello();
personB.sayHello();

Unlike constructor functions, classes are a pretty common concept that exists in many other programming languages. JavaScript initially didn't have any support for classes. It was added to the language at a later stage. The classes in JavaScript and other languages have some fundamental differences, but we won't spend too much time with either classes or with constructor functions so that discussion is beyond the concerns of this book.

Summary

An object is a data type that allows us to organize other data together. Objects have properties and methods. We can think of properties as defining a value on the object, whereas methods define the object's behaviors. We can use the dot notation or the square bracket notation when working with objects. Using dot notation makes it easier to type but doesn't work when we want to use numbers or variables as property or method names.

We can use the this keyword to refer to the object itself. Remember not to use arrow functions when defining methods since they don't work well with the this keyword.

One fundamental concept to be aware of when working with objects is how they are passed around. Objects are passed by reference, whereas other data types that we have seen so far, like numbers or strings, are passed by value. This has implications on assigning objects to variables. If we want to create a new copy of an object when assigning it to a variable, we should be using the spread operator. It is important to remember that the spread operator only creates a shallow copy of the object.

Finally, we have looked into creating an object template that we can use to create multiple objects (object instances) easily. We have learned about factory functions, constructor functions, and classes used for this purpose.

Find me on Social Media