Prototype in JavaScript: it‘s quirky, but here‘s how it works

If you‘ve been programming in JavaScript for a while, you‘ve likely encountered the following quirky behavior:

Object instanceof Function // true
Object instanceof Object   // true
Function instanceof Object // true 
Function instanceof Function // true

At first glance, these results seem paradoxical. How can Object be an instance of Function and itself? And likewise for Function? The answer lies in JavaScript‘s prototype system, a core aspect of the language that enables inheritance and shared behavior between objects.

In this post, we‘ll dive deep into how prototypes work in JavaScript. We‘ll explore what the prototype object is, how it relates to constructor functions, and the way prototypes enable an inheritance chain between objects. By the end, you‘ll have a solid understanding of this fundamental JavaScript concept. Let‘s get started!

Objects and Functions in JavaScript

Before we jump into prototypes, let‘s review the foundations: objects and functions. In JavaScript, there are two basic categories of data types:

  1. Primitives: undefined, null, boolean, number, string, symbol
  2. Objects: collections of key-value pairs, including plain objects, arrays, functions, dates, regexes, and more

Objects are created via constructor functions or literal notation. The built-in Object constructor creates a plain object:

let obj1 = new Object();
let obj2 = {}; // object literal, functionally the same as above

Functions are special objects that can be invoked. They are created via the Function constructor or function declaration/expression syntax:

let func1 = new Function(‘a‘, ‘b‘, ‘return a + b‘);
function func2(a, b) { return a + b; } // function declaration
let func3 = (a, b) => a + b; // arrow function expression

The fact that functions are objects allows them to be passed as arguments, returned from other functions, assigned to variables, and hold their own properties, just like any other object. Keep this in mind as we explore prototypes.

The Prototype Object

Here‘s the key to understanding JavaScript‘s prototype system: Every function has a special "prototype" object property.

When you create a function, JavaScript automatically assigns it a prototype object that is initially empty (apart from a "constructor" property that points back to the function). You can add properties and methods to this prototype object as needed.

Here‘s an example:

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

Person.prototype.sayHello = function() {
  console.log(‘Hello, my name is ‘ + this.name);
};

let john = new Person(‘John‘);
john.sayHello(); // Hello, my name is John

In this code, we define a Person constructor function and add a sayHello method to its prototype. When we create an instance of Person called john, it inherits the sayHello method from Person.prototype.

[[Prototype]] and prototype

Here‘s where things get a bit confusing: Every object has an internal [[Prototype]] property that points to another object or null.

When you create an object via a constructor function using the new keyword, that object‘s [[Prototype]] gets set to the constructor‘s prototype property. In other words:

john.__proto__ === Person.prototype // true

The [[Prototype]] is an internal property, but most environments expose it via the proto accessor property. It‘s important to note that proto is non-standard and deprecated in favor of Object.getPrototypeOf() and Object.setPrototypeOf(), but it‘s still widely used and supported.

So in summary:

  • Functions have a prototype property that starts as an empty object
  • Objects have an internal [[Prototype]] that points to the prototype object of their constructor

These two distinct but related concepts link objects via an inheritance chain known as the "prototype chain."

The Prototype Chain

The prototype chain is how JavaScript implements inheritance and shared properties/methods between objects. When you access a property or method on an object, JavaScript first looks for it on the object itself. If it‘s not found, it traverses up the prototype chain, checking the linked objects in [[Prototype]] until it finds the property/method or reaches the end of the chain.

Here‘s a concrete example:

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

Animal.prototype.eat = function() {
  console.log(this.name + ‘ is eating.‘);
};

function Bird(name) {
  Animal.call(this, name);
}

Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;

Bird.prototype.fly = function() {
  console.log(this.name + ‘ is flying.‘);
};

let tweety = new Bird(‘Tweety‘);
tweety.eat(); // Tweety is eating.
tweety.fly(); // Tweety is flying.

In this example, we define an Animal constructor with an eat() method, and a Bird constructor that inherits from Animal. The key lines are:

Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;

This sets up the prototype chain so that Bird.prototype‘s [[Prototype]] points to Animal.prototype. We also restore Bird.prototype.constructor to the Bird function, since Object.create() will have set it to Animal.

Now when we call tweety.eat(), JavaScript first looks for an eat property on the tweety object itself. When it doesn‘t find one, it follows tweety.__proto__ to Bird.prototype. eat() isn‘t found there either, so it goes up one more link to Animal.prototype, where eat() is finally found and executed.

The prototype chain is the backbone of inheritance in JavaScript. By linking objects through their [[Prototype]]s, we can implement hierarchies and shared behavior.

instanceof and the Prototype Chain

The instanceof operator uses the prototype chain to check whether an object is an instance of a given constructor. It traverses the object‘s prototype chain looking for the constructor‘s prototype property. If found, instanceof returns true.

Going back to our initial quirky examples:

Object instanceof Function // true

This is true because Object.__proto__ points to Function.prototype. In other words, the Object constructor function inherits from Function.prototype.

Object instanceof Object // true

This is also true because Object.__proto.\proto__ points to Object.prototype. Object inherits from Function.prototype, which in turn inherits from Object.prototype.

The same logic applies to Function instanceof Function and Function instanceof Object. Functions are objects in JavaScript, and all functions descend from the primordial Function constructor.

Object and Function Prototypes

Let‘s take a closer look at the prototypes of the built-in Object and Function constructors.

Object.prototype is the top of the prototype chain for most objects. Its prototype is null:

Object.prototype.__proto__ === null // true

Function.prototype inherits from Object.prototype:

Function.prototype.__proto__ === Object.prototype // true

And Function.prototype‘s constructor points back to the Function constructor:

Function.prototype.constructor === Function // true

So putting it all together, we have:

  • Object.__proto__ === Function.prototype
  • Function.__proto__ === Function.prototype
  • Function.prototype.__proto__ === Object.prototype
  • Object.prototype.__proto__ === null

This circular relationship between Object and Function is a key reason why instanceof returns true for the quirky examples we saw earlier.

Primitives and Prototypes

You might be wondering: If primitives aren‘t objects, how do they work with prototypes and methods like .toString() and .valueOf()?

The answer is that when you try to access a property or method on a primitive value, JavaScript temporarily wraps the value in a corresponding object wrapper (String for strings, Number for numbers, etc.) This is known as "boxing."

These object wrappers inherit from their respective constructor‘s prototype, which is why primitives can access methods like String.prototype.toUpperCase() and Number.prototype.toFixed().

Here‘s an example:

let str = ‘hello‘;
console.log(str.toUpperCase()); // ‘HELLO‘

Behind the scenes, JavaScript is essentially doing this:

let str = new String(‘hello‘);
console.log(str.toUpperCase()); // ‘HELLO‘
str = null; // discard the temporary wrapper object

So while primitives aren‘t objects, they can still benefit from prototypes and shared methods via auto-boxing.

Implementing Inheritance with Prototypes

We saw a simple example of inheritance with Animal and Bird constructors earlier. Let‘s expand on that to illustrate a more complex prototype chain.

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

Animal.prototype.eat = function() {
  console.log(this.name + ‘ is eating.‘);
};

function Bird(name) {
  Animal.call(this, name);
}

Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;

Bird.prototype.fly = function() {
  console.log(this.name + ‘ is flying.‘);
};

function Penguin(name) {
  Bird.call(this, name);
}

Penguin.prototype = Object.create(Bird.prototype);
Penguin.prototype.constructor = Penguin;

Penguin.prototype.swim = function() {
  console.log(this.name + ‘ is swimming.‘);
};

let skipper = new Penguin(‘Skipper‘);
skipper.eat();  // Skipper is eating.
skipper.fly();  // TypeError: skipper.fly is not a function
skipper.swim(); // Skipper is swimming.

In this expanded example, we have a three-level prototype chain: Penguin inherits from Bird, which inherits from Animal.

Penguin instances get the swim() method from Penguin.prototype, and the eat() method from Animal.prototype. However, they don‘t inherit the fly() method from Bird.prototype, since we specifically override that behavior for penguins.

This showcases the power and flexibility of prototypal inheritance in JavaScript. You can selectively inherit or override properties and methods at each level of the prototype chain to model your desired object hierarchies.

Conclusion

JavaScript‘s prototype system is undeniably quirky, but it‘s a powerful and fundamental part of the language. Understanding how prototypes work—the relationship between constructor functions, prototype objects, and the [[Prototype]] linkage—is key to mastering object-oriented programming in JavaScript.

Some key takeaways:

  • Every function has a prototype object that‘s used for inheritance.
  • Every object has an internal [[Prototype]] that points to the prototype object of its constructor.
  • The prototype chain links objects via [[Prototype]] and enables property/method lookups and inheritance.
  • The instanceof operator traverses the prototype chain to check an object‘s lineage.
  • Object and Function have a circular prototype relationship at the top of most prototype chains.
  • Primitives use auto-boxing and object wrappers to work with prototypes and shared methods.

While this post covered the essential concepts, there‘s certainly more to explore with prototypes and inheritance in JavaScript. But armed with this knowledge, you‘re well on your way to writing more expressive, flexible, and maintainable code. Happy prototyping!

Similar Posts