As someone who learned to code in C++, JavaScript was confusing to me when I first started learning it. One of the main reasons for my confusion was that the object-oriented concepts I was used to were seemingly nowhere to be found. Sure, you could create objects in JavaScript, but to me, they seemed more like a map
in C++ or a dict
in Python.
I certainly didn't understand how inheritance could work, and for a while, I just assumed that inheritance wasn't possible in JavaScript. Turns out, inheritance is possible in JavaScript, but it is done through the use of prototype objects.
Prototype Objects
The Mozilla docs explain prototype objects like this:
When it comes to inheritance, JavaScript only has one construct: objects. Each object has a private property which holds a link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with
null
as its prototype. By definition, null has no prototype, and acts as the final link in this prototype chain.
So, in JavaScript, there is no such thing as a class
in the traditional sense. There are only objects. Each object has an attribute called a prototype that you can access via obj.__proto__
. When you try to access an attribute on an object instance, JavaScript will first check the attributes of that object. If the attribute is not found there, it will check the object's prototype, then the prototype's prototype, and so on, until it finds the attribute or reaches the end of the prototype chain.
An Example
To define an object type in an object-oriented language like Java or Python, you would create a class and give it a special method called a constructor. For example, to define a Product
object type in Java, you could write
public class Product {
double price;
public Product(double price) {
this.price = price;
}
}
To define an object type in JavaScript, you only create the constructor. For example, we could create a Product
constructor like this.
function Product(price) {
this.price = price
}
To create a new object using the Product
constructor, you would do
let product = new Product(10)
When you call new
in this line, the variable product
gets injected as this
in the Product
constructor. Therefore, if you log the output of product.price
you should get 10
.
Now what if we wanted to add a method to all instances of Product
? In Java, we would add a method to the class, like this.
public class Product {
double price;
public Product(double price) {
this.price = price;
}
public String sayPrice() {
return "I cost $" + String.valueOf(this.price);
}
}
In JavaScript, we add the method as an attribute on Product
's prototype
.
Product.prototype.sayPrice = function() {
return `I cost $${this.price}`
}
We could now call sayPrice
on the product
instance.
product.sayPrice()
// Output: I cost $10
If you log Product.prototype
and product.__proto__
you will notice that they are the same object. When JavaScript sees that sayPrice
is not an attribute of product
, it checks its __proto__
and finds the sayPrice
method there.
Inheritance
Now suppose we wanted to define a Book
object type that inherits from Product
. In Java, we would extend the base class.
public class Book extends Product {
int pages;
public Book(int pages, double price) {
super(price);
this.pages = pages;
}
}
In JavaScript, we would create a Book
constructor, inside of which we call the Product
constructor, passing this
as the context.
function Book(pages, price) {
Product.call(this, price)
this.pages = pages
}
Now if we create a book with
let book = new Book(154, 10)
we will get an object that has both the price
and pages
attributes. However, if we try to call
book.sayPrice()
we will get a TypeError: book.sayPrice is not a function
. This is because we haven't updated Book
's prototype
object. To fix this, we can do
Book.prototype = Object.create(Product.prototype)
This sets Book
's prototype
to an empty object whose prototype is Product.prototype
. In other words,
Book.prototype.__proto__ === Product.prototype
// Output: true
We can now run
let book = new Book(154, 10)
book.sayPrice()
// Output: I cost 10
and JavaScript will find sayPrice
on book.__proto__.__proto__
.
There is one other thing I should mention. By default, every constructor's prototype
is an object with one attribute, constructor
, which is a reference to the constructor itself. You can check this by running
Product.prototype.constructor === Product
// Output: true
When we set Book.prototype = Object.create(Product.prototype)
, we overwrote Book
's default prototype
, so book.constructor === Product
, which isn't correct. We can fix this by either adding the constructor
property afterwards
Book.prototype = Object.create(Product.prototype)
Object.defineProperty(Book.prototype, 'constructor', {
value: Book,
enumerable: false,
writable: true
})
or by replacing the Object.create
line with Object.assign
Book.prototype = Object.assign(Book.prototype, Product.prototype)
Now Book.prototype.constructor === Book
should be true.
What About JavaScript's class
Keyword?
JavaScript does have a class
keyword, but it's just syntactic sugar for what we did above. It allows you to create the constructor and set methods on the prototype
all in one block. For example, we could create an equivalent Product
type with
class Product {
constructor(price) {
this.price = price
}
sayPrice() {
return `I cost $${this.price}`
}
}
If you log Product.prototype
, you'll see that it is an object with both constructor
and sayPrice
properties, just like we had before. We can also create the Book
sub type with
class Book extends Product {
constructor(pages, price) {
super(price)
this.pages = pages
}
}
Defining Book
this way will automatically set Book.prototype
and ensure that book.constructor
is correct.
Conclusion
Objects in JavaScript are a bit confusing at first since they are quite different than in most other languages. I encourage you to open the dev tools in your browser and play around with these examples to get a feel for them. I hope this helps you understand JavaScript a little better!
Top comments (0)