A simple explanation of JavaScript’s weird bits: Prototypes, constructor functions, and ES6 Classes

Josh Stillman
8 min readApr 19, 2018

--

Coming to JavaScript from a background in Ruby (and long before that, C++), I found that JavaScript had a lot of weird bits — features of the language that felt very unfamiliar coming from class-based object-oriented languages. In these other languages, a class is a sort of abstract blueprint for creating instances of that class. The blueprint is not itself an instance of the class and an instance of the class is not itself the blueprint, although its attributes are shaped by the blueprint. Forgive the liberal arts major in me, but it’s all very Platonic: for Plato, the “form” of a dog is a sort of abstract blueprint or template for making particular dogs. The “form” contains the essential features of any dog, while a “particular” dog is an instantiation and expression of the “form” of the dog in a living, breathing, material being, partaking in the qualities of the “form.” (Sounds a lot like object-oriented programming in class-based languages!)

Ruby: Strictly Platonic…

JavaScript is a different beast entirely. It’s not a class-based language, and does not have classes and instances in the same way that Ruby or C++ does. With the class syntax introduced in ES6, it’s possible to pretend as if JavaScript works in the same way as those languages, but the class syntax is just syntactic sugar for constructing objects the old-fashioned JavaScript way: with constructor functions and prototypes. To fully understand JavaScript, it’s important to peek under the hood, kick the tires, and see what’s actually happening when you use ES6 class syntax.

While there’s a lot of good long-form material out there on this topic (shout out to MPJ’s awesome videos at Fun Fun Function), I didn’t find a super quick explanation that clicked for me. So, having already done the deep dive for you, here’s my attempt at explaining JavaScript prototypes, constructor functions, and classes as simply and quickly as possible.

What’s a prototype?

Recall that in JavaScript, almost everything is a POJO — a plain ol’ JavaScript object, similar to a hash in Ruby: {}. It is a data structure encapsulating a series of key/value pairs, and it is denoted by a pair of curly brackets (or “curly bronsons” if you prefer). Here’s a JavaScript object:

let child = {
name: "bob",
age: 5,
belch: function(){console.log("burp")}
}

What happens when we attempt to access a property or method on the child object? The JavaScript interpreter will access the child object and determine if the property we attempted to access exists on that object; if it does, the interpreter will return the requested value. Thus, child.name will return "bob".

But suppose we wanted to share functionality and data between different objects. Let’s say there’s a second object called parent , and we want to give child access to parent's attributes and methods.

let parent = {
hair: "blond",
sayHello: function(){console.log("hello")}
}

We can set up a way for child to access parent’s attributes and methods by use of a foundational JavaScript feature called prototypes. Prototypes essentially are a way of delegating or passing the buck to another object: the object basically says, if you don’t find what you’re looking for on me, go check for it on my prototype.

First, we’ll set parent to be the prototype of child like so: child.__proto__ = parent. (This can also be done with Object.setPrototype(child, parent).) Now what happens when we attempt to access child.hair?

First, the JS interpreter will look in the child object itself to see if a hair property has been defined. Finding no such property on child, the JS interpreter will then look to the object that has been set as child's prototype (here, parent). Finding a hair property there, the interpreter will return it. So, after parent has been set as child's prototype, child.hair will return "blond." Here’s a dramatization in code:

child.__proto__ = parent   // This sets a pointer from child to parent, telling the 
JS interpreter, if you don't find an attribute on me, then go
look for it on parent, my prototype.
child.hair //JS interpreter: Hmm, I don't see a hair property on the
child object. Before I return "undefined," let's check and
see if child's prototype (here set to the parent object) has what
I'm looking for.
//JS interpreter looks at parent.hair: Oh, ok, parent.hair exists.
So I'll return that value when child.hair is called!
=> "blond"

In this way, attributes and functionality can be shared between objects without duplicating them in memory. We don’t need to create two strings containing "blond" and attach them to two different objects at the key of hair:; nor do we need to create two identical sayHello() functions on two different objects — which would be a very inefficient, non-DRY approach. Rather, here, we create one string and one function, and share access to them via the .__proto__ pointer. Again, by setting an object’s prototype, the object tells the JS interpreter: if you don’t find an attribute (such as.hair) on me, check for it on my prototype!

Note that the prototype is a particular, existing JavaScript object. It is not a copy of that object, nor is it any sort of abstract blueprint or a class like in Ruby or other class-based languages. The .__proto__ property is just a pointer to that particular object. We can see that in the code below, where we directly access and modify an object’s prototype object through its .__proto__ pointer.

parent.hair // "blond"
child.hair // "blond"
child.__proto__ === parent // true
child.__proto__.hair // "blond"
child.__proto__.hair = "red"
child.hair // "red"
parent.hair // "red"

Woah! Because .__proto__ is a pointer to a particular object, we can modify that object via the pointer. And when we call child.hair after modifying that property, the pointer will point to the modified attribute on the prototype.

The Prototype Chain

This prototype relationship can (and often does) have more than one level of delegation (or buck passing). Indeed, if we were to call child.height, the JS interpreter would look on child, not finding it there, look at its prototype, parent, and not finding it there either, look at parent’s prototype. What’s parent’s prototype? In this case, it’s the global Object.prototype, from which all objects inherit functionality by delegation. Not finding a .height defined on Object.prototype, which is the last stop in the prototype chain, the JS interpreter will return undefined.

But what about about calling child.hasOwnProperty(“name”) (which here will return true)? That method isn’t defined on child, nor on child’s prototype, parent. But it is defined on parent’s prototype, the global Object.prototype. Through this chain of prototypical inheritance, child gains access to the .hasOwnProperty method.

Note that the JS interpreter will stop going further up the chain as soon as it finds a matching attribute. Thus, here, if it finds a .hasOwnProperty attribute or method on child, it will return that value for child.hasOwnProperty instead. It won’t need to keep going further up the prototype chain and look at child’s prototype (parent), or that object’s prototype (the global Object.prototype) to find the requested attribute:

child.hasOwnProperty === Object.prototype.hasOwnProperty // true
child.hasOwnProperty = "asdf"
child.hasOwnProperty // "asdf"
child.hasOwnProperty === Object.prototype.hasOwnProperty // false

So that’s the basics of how prototypes work in JavaScript. An object’s .__proto__ attribute is a pointer to another object, which may itself have a .__proto__ attribute pointing to yet another object. The end of this chain, however long it may be, will be the global Object.prototype. When we request an object’s attribute or method, the JS interpreter will start looking for it on the object itself, then follow the prototype chain until it finds a matching attribute. If it goes all the way up the chain to the global Object.prototype and still doesn’t find the requested attribute or method, it will return undefined. That’s a Javascript prototype!

What are constructor functions?

A constructor function is a function that, when called with the keyword new, creates brand new objects that all share the same prototype object. In the example below, Dog.prototype is just another object {}, but that object will be set as the prototype of any new objects created with the constructor function, giving them access to the functionality and data defined on the Dog.prototype object.

function Dog(name, age){
this.name = name
this.age = age
}
Dog.prototype.bark = function(){console.log("woof")}let fido = new Dog("fido", 4)fido.name // "fido"
fido.age // 4
fido.bark() // "woof"

What’s going on when we call a constructor function with new? Four things: 1. We are creating a new plain ol’ JavaScript object {}.
2. We are essentially passing the new object into the constructor function as an implied argument, setting this in the constructor function equal to that new object, and then executing the constructor function.
3. We are setting the new object’s .__proto__ to point to the constructor function’s .prototype object (which here has the bark() method defined on it).
4. We are returning the newly created object.

In this way we can make new objects with constructor functions that share functionality defined on the constructor function’s .prototype object. Every new dog created with the constructor will share access to the very same bark() method defined on the constructor function’s .prototype object, since it will be set as the prototype for each new dog object.

One major point of confusion: what’s the difference between .__proto__ and .prototype? We learned above that the .__proto__ attribute on an object is a pointer pointing to its prototype object. In contrast, the .prototype attribute on a constructor function is a special property of constructor functions. The constructor function’s .prototype also points to an object, but here it is the object that will be used as the prototype for new objects created by the constructor function. So in the example above, fido.__proto__ points to the Dog.prototype object, which was “installed” as fido’s prototype when fido was created in the constructor function. Think of .__proto__ as asking any object “who’s your prototype?”, whereas .prototype is specifically asking constructor functions, “when we create a new object with you, what object will you set as the prototype for those new objects?”

If this is still confusing (the confusing naming certainly doesn’t help), try playing around with the Dog constructor function code above in the console to see how it works and what happens when you makes changes to Dog.prototype.

ES6 Class Syntax

Before ES6 came along, constructor functions were primarily how one did object-oriented programming in JavaScript. ES6 introduced the class syntax, but it’s important to understand that this is purely syntactic sugar. Under the hood, JavaScript is still just using constructor functions and prototypes as we did above to create supposed “classes” and “instances.” But JavaScript still is not a true class-based language. So:

class Dog {
constructor(name, age){
this.name = name
this.age = age
}
bark(){
console.log("woof")
}
}

is equivalent to:

function Dog(name, age){
this.name = name
this.age = age
}Dog.prototype.bark = function(){console.log("woof")}

In sum, don’t be misled by the class syntax. We’re still dealing with a chain of prototypical inheritance, in which objects delegate to other objects (i.e., if you don’t find it on me, check for it on my prototype!), rather than a true class-based system of abstract blueprints and particular instances thereof.

Conclusion

So there you have it, my attempt at a quick(ish) explanation of prototypes, constructor functions, and ES6’s class syntax. While this stuff is less-than-immediately-intuitive coming from class-based object-oriented languages, I found that playing around with objects’ .__proto__ attributes, constructor functions’ .prototype objects, and seeing how prototypes function under different circumstances was the most helpful way to see what was going on under the hood. Happy coding!

--

--