Intro to object-oriented JS

We're in the middle of making an application for a school to manage their students. For each student we want to keep track of their student id, name, and what subjects they are taking this school year.

We've got a couple of students here, student1 and student2, and we've expressed all of this data with objects using properties.

Students will be able to add and change the subjects they are taking, so we want to make that possible in our code. And with objects, we already know that we can add behavior with methods.

So to be able to add subjects as we like, let's create a method called addSubject. We'll be able to pass this function a subject that the student is taking and it will add it to their list of array of subjects. We'll use the spread operator to spread all of the previous subjects into a new array, add the new one at the end and update the subjects property.

const student1 = {
  id: 1,
  name: "Reed",
  subjects: [],
  addSubject(subject) {
    this.subjects = [...this.subjects, subject];
  },
};

And within this method, since we know that the this keyword is equal to the object that the method is on, we can get the previous subjects from this.subjects and set the new array to this.subjects as well

Let's try adding a subject. Let's say student1 has signed up for gym class. We can say:

student1.addSubject("Gym");

And log the subjects array afterwards

console.log(student1.subjects); // 'Gym'

And we see that this works. Great, we know now that all students can use this method to update the subjects they are taking. We can add this method to all of our students.

Looking ahead, however, can you see the problem? Think about if we had 1000 students instead of just 2.

The problem is that if we had 1000 students already, we would need to manually add this method to all of our student objects. Imagine how much work that would be. Also think about if we had more than one method to add.

So the question now is--what's a better way to create objects on demand and immediately provide behavior to them, such as our addSubject method without having to directly add them on the object?

Constructor functions

In fact, there is a special type of function that JS makes available to us that enables us to construct objects on demand that all share the properties and methods we want them to have. This function is appropriately named a constructor function.

// constructor function

Let's create a normal JS function with the function keyword. This is required in making a constructor function. And since we are creating student objects, let's appropriately call this function Student. Note that the convention is to use a capital letter to indicate this is not just a normal function, but will be used to create objects.

Also, the reason we don’t call it createStudent or something like it was a regular function is that the constructor function represents the data (objects) it makes, not a specific operation like most functions.

function Student() {}

Now to model the student data that's going to be on each student object, we can again use the this keyword. this will represent the object that will be created by the constructor function, so we can just directly set whatever properties we want to be on the created objects on this.

These values are going to be passed in to the function to make new students, so we know to make these values parameters and put them on their appropriate property.

Know also that we can provide default values for any of these parameters. We'll make subjects an empty array by default.

function Student(id, name, subjects = []) {
  this.id = id;
  this.name = name;
  this.subjects = subjects;
}

And that’s it. Note that we are not returning these values, we are just putting them on the objects we want to make.

Now let’s make our first student. The important thing is that we call every constructor function with the new keyword along with our arguments:

new Student(1, "Reed"); // Student

So what happened? First of, all we created a student object, or more precisely, we instantiated or created a new instance of Student.

What new keyword does

And this was the result of using the new keyword. You can see that's the case if you were to execute the same function without new. You get a return value of undefined.

console.log(Student()); // undefined

In fact, this is a downside to working with constructor functions. If you don't call new, you won't get an error and it won't work as you expected.

With new, the object instance is implicitly returned from the function. It is as if we added ‘return this’ to the end of the function.

Prototype

So this is great, we have our first student again and we can use it to make any number of students with the same exact properties, so making objects is a lot easier, but what about giving them the same behavior, like our addSubject method?

Using constructor functions also give us the ability to share functionality through a special property called the prototype.

And before we can understand the prototype property, we have to understand that functions are just special JavaScript objects. As a result, functions can have properties just like any other JS type. This can trip people up, but it is important to understand.

With that, let's add back our addSubject method. We can do so by adding a function to the prototype. To do that, we take the constructor function, selecting the prototype property on it and chaining on the new function:

Student.prototype.addSubject = function () {};

Always use function declarations for prototype methods

We always need to use a regular function declaration here declared with the function keyword if we want to access data from the object itself. We should not use an arrow function.

Think back to when we spoke about arrow functions and try to give an explanation as to why this might be, I’ll give you a second to do so.

The reason is that arrow functions don’t get access to this in the current context, but they go a level up. Therefore, it wouldn’t refer to the object created by the constructor function, but the parent context.

Let's rewrite addSubject:

Student.prototype.addSubject = function (subject) {
  this.subjects = [...this.subjects, subject];
};

Prototype of all objects changed immediately

What will happen now?

Now once the constructor function prototype has been updated, immediately all of the created objects have access to the added method directly on them.

So if we take student1, we can call addSubject on it, just like we did before:

student1.addSubject("Recess");

Let's try making a new student and if the method's there too:

const student2 = new Student(2, "Doug");
student2.addSubject("Recess");
console.log(student2.subjects); // ['Recess']

And we get our updated subjects array, so it is.

So how does this magical prototype property work and how does it immediately pass properties or methods to all of our objects that were created by the function constructor?

We'll cover that in the next article.

Challenge

As a challenge, make a new object, student3, using the our Student constructor function and also add a method on the prototype called removeSubject that allows you to remove elements from the subjects array that you pass to the function. Remember back to the arrays section and think of the method that allows us to filter out certain elements