All You Need to Know to Understand JavaScript‘s Prototype
If you‘re coming from a class-based, object-oriented programming language like Java or C++, JavaScript‘s prototypal inheritance model likely seems foreign and perhaps a little bizarre. Instead of creating classes and instantiating objects from them, JavaScript relies on prototype chains to share behavior between objects.
However, truly understanding prototypes is key to mastering JavaScript as a language. It‘s a fundamental concept that powers the language‘s object system and enables powerful patterns for code reuse and object composition. In this comprehensive guide, we‘ll dive deep into everything you need to know about prototypes in JavaScript.
The History and Theory of Prototypal Inheritance
First, let‘s discuss why JavaScript uses prototypes in the first place. When Brendan Eich created JavaScript in 1995, he wanted to build an object system that was more dynamic and flexible than the rigid class hierarchies of Java or C++. He was inspired by languages like Self and Smalltalk that used prototypes rather than classes.
The core idea behind prototypal inheritance is that objects can directly inherit from other objects, without the need for an intermediary class definition. Any object can specify another object as its prototype, and it will automatically have access to all of that object‘s properties and methods.
This approach provides more flexibility at runtime. Objects can inherit from different prototypes or even change their prototype on the fly. It also enables objects to serve as "exemplars" that can be cloned and extended.
Constructors, Instances, and Prototypes
In practice, most JavaScript code uses constructor functions to create objects with shared behavior. A constructor is simply a function that‘s meant to be called with the new
keyword to instantiate a new object. By convention, constructors are capitalized to distinguish them from regular functions.
Here‘s a simple constructor for a Person
object:
function Person(name, age) {
this.name = name;
this.age = age;
}
When you call this with new
, it will create a new object and bind this
within the constructor to that new object:
let john = new Person("John", 30);
console.log(john.name); // "John"
console.log(john.age); // 30
But what if we want all Person
objects to share some common functionality? That‘s where the constructor‘s prototype
property comes in. Every function in JavaScript automatically has a prototype
property that starts out as an empty object. When you call a function as a constructor with new
, the newly created object‘s prototype will be set to the constructor function‘s prototype
property.
We can add shared methods to the Person.prototype
:
Person.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name);
};
Now all instances of Person
will have access to the sayHello
method:
john.sayHello(); // "Hello, my name is John"
let jane = new Person("Jane", 25);
jane.sayHello(); // "Hello, my name is Jane"
Prototype Chains and Object.create
What makes prototypes truly powerful is that an object‘s prototype can itself have a prototype, forming a prototype chain. When you try to access a property on an object, JavaScript will traverse up the prototype chain, looking for the first place where that property is defined.
We can use Object.create
to create a new object with a specific prototype:
let personPrototype = {
sayHello: function() {
console.log("Hello, my name is " + this.name);
}
};
let john = Object.create(personPrototype);
john.name = "John";
john.sayHello(); // "Hello, my name is John"
Here, john
doesn‘t have a sayHello
method of its own, but it inherits it from its prototype. We can continue this chain as long as we want:
let studentPrototype = Object.create(personPrototype);
studentPrototype.sayGoodbye = function() {
console.log("Goodbye!");
};
let jane = Object.create(studentPrototype);
jane.name = "Jane";
jane.sayHello(); // "Hello, my name is Jane"
jane.sayGoodbye(); // "Goodbye!"
Now jane
inherits from studentPrototype
, which in turn inherits from personPrototype
. When we call jane.sayHello()
, JavaScript first looks for a sayHello
property on jane
. When it doesn‘t find one, it looks on jane
‘s prototype, which is studentPrototype
. It doesn‘t find sayHello
there either, so it looks on studentPrototype
‘s prototype, which is personPrototype
, and finds sayHello
there.
ES6 Classes: Syntactic Sugar for Prototypes
In 2015, ECMAScript 6 introduced a class
syntax to JavaScript. This made it easier to create constructor functions and establish prototype chains, but it‘s important to understand that under the hood, the prototype system is still being used.
Here‘s how we could rewrite our Person
and Student
examples using the class
syntax:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("Hello, my name is " + this.name);
}
}
class Student extends Person {
constructor(name, age, major) {
super(name, age);
this.major = major;
}
sayGoodbye() {
console.log("Goodbye!");
}
}
When you use the extends
keyword, it sets up the prototype chain for you under the hood. Student.prototype
will automatically be an object that inherits from Person.prototype
.
While the class
syntax is generally preferred in modern JavaScript for its simplicity and clarity, it‘s crucial to understand that it‘s just syntactic sugar over the prototype system.
Prototypes and Performance
One important consideration when using prototypes is performance. When you access a property on an object, JavaScript has to traverse up the prototype chain to find it. The more levels deep the property is in the chain, the longer this lookup takes.
In modern JavaScript engines, this performance hit is generally negligible for most use cases. However, in performance-critical code or when dealing with very large prototype chains, it can start to add up.
One way to mitigate this is to cache prototype properties in a local variable:
function logName() {
let getName = this.getName;
console.log(getName());
}
Instead of accessing this.getName
every time, we cache it in a local getName
variable. Local variables are much faster to access than prototype chain lookups.
However, the best approach is generally to keep your prototype chains as shallow as possible and to be mindful of how you structure your objects. Avoid deeply nested hierarchies in favor of composition and delegation.
Prototypes in the Wild
Prototypes are used extensively in JavaScript libraries and frameworks. Understanding how they work is crucial to using these tools effectively.
For example, in jQuery, every DOM element that you select with $()
is an instance of the jQuery
object. All of the methods like addClass
, on
, and css
are defined on jQuery.prototype
. This is what allows you to chain method calls like $(‘button‘).addClass(‘active‘).on(‘click‘, ...)
.
In frameworks like Backbone.js, prototypes are used to share behavior between instances of Models, Collections, and Views. Backbone.Model.extend sets up a prototype chain for your model classes, allowing instances to inherit default functionality.
Understanding prototypes is also crucial when using Node.js and interacting with built-in modules. For instance, every HTTP request in an Express server is an instance of the http.IncomingMessage
object, and every response is an instance of http.ServerResponse
. Knowing how to extend and manipulate these prototypes is a key part of building Node applications.
Expert Tips and Best Practices
Here are a few tips and best practices to keep in mind when working with prototypes in JavaScript:
-
Use
Object.create
for cleaner prototype chaining. Instead of mutating an object‘s__proto__
directly, useObject.create
to create a new object with the desired prototype. -
Set methods on prototypes, not instances. If a method doesn‘t use any instance-specific data, define it on the prototype instead of the instance. This allows instances to share a single function instead of each having its own copy.
-
Be careful when extending built-in prototypes. While it‘s possible to add methods to prototypes of built-ins like
Array
orString
, it can lead to confusion and hard-to-debug issues. Generally, it‘s best to avoid modifying prototypes you don‘t own. -
Use prototypes for shared behavior, not shared state. Prototypes are great for sharing methods, but you should generally avoid putting mutable data on a prototype. Instances will end up sharing that data, which is rarely what you want.
-
Understand the difference between
__proto__
andprototype
. An object‘s__proto__
property points to its constructor‘sprototype
property. Theprototype
property is only relevant for functions that will be used as constructors.
Further Reading
If you want to dive even deeper into prototypes and inheritance in JavaScript, here are a few excellent resources:
- MDN‘s guide to object prototypes
- Eric Elliott‘s "Composition Over Inheritance"
- Dr. Axel Rauschmayer‘s "Speaking JavaScript" chapter on prototypes
- Kyle Simpson‘s "You Don‘t Know JS: this & Object Prototypes"
Understanding prototypes is a rite of passage for any JavaScript developer. It‘s a concept that‘s essential to writing idiomatic, efficient JavaScript code and to working with the language‘s many libraries and frameworks.
Hopefully, this guide has demystified prototypes and given you a solid foundation for using them in your own code. Remember: prototypes are just objects that other objects can inherit from. Constructors let you set up that inheritance relationship. And the prototype chain is what allows objects to access inherited properties.
Happy coding!