The Deal With Prototypes
Level: Beginner
Prototypes in JavaScript are objects that other objects look to when they can’t find properties that are being requested of them. If the object doesn’t have the property itself, it can instead use the value of that property in its associated prototype.
You can set the prototype for an object using Object.setPrototypeOf()
.
let me = {};
let friend = { answer: 42 };
console.log(me.answer); // undefined
Object.setPrototypeOf(me, friend);
// (me's prototype is now friend)
console.log(me.answer); // 42 (taken from friend)
Prototypes are the mechanism by which JavaScript handles object inheritance. In the example above, the me
object was set to inherit from the friend
object. By inheriting from friend
, me
has access to all of the properties in friend
.
Whenever you create a new object normally, that object will already inherit values from the Object type. This includes methods like toString()
.
let empty = {};
console.log(empty.toString()); // [object Object]
// (toString is inherited from empty's default prototype)
When an object inherits properties from its prototype, it also inherits the properties in its prototype’s prototype since prototype objects will also look to their own prototypes if a property is being accessed that they don’t have. This creates the prototype chain. In the earlier example where me
's prototype was set to friend
, me
still had access to all of the values inherited from the Object type because friend
inherits from it by default.
let me = {};
let friend = { answer: 42 };
Object.setPrototypeOf(me, friend);
console.log(me.toString()); // [object Object]
// (me's toString inherited from friend
// which inherited it from the Object type)
New objects can be created with a specific prototype already set using Object.create()
.
let friend = { answer: 42 };
let me = Object.create(friend);
console.log(me.answer); // 42
To get the prototype associated with an object, use Object.getPrototypeOf()
.
let friend = { answer: 42 };
let me = Object.create(friend);
console.log(Object.getPrototypeOf(me) === friend); // true
There’s also an __proto__
accessor property (inherited from the Object type), but it’s use is not recommended. However, when digging through object hierarchies in debuggers like the one used in chrome, you’ll see this identifier used to provide access to the prototype of an object so it’s good to know.
let me = {};
let friend = { answer: 42 };
me.__proto__ = friend;
console.log(me.answer); // 42
console.log(Object.getPrototypeOf(me) === me.__proto__); // true
Object prototypes must be an object or null
. Non-null primitive values are not allowed. If you create an object with a null
prototype (or set it to null
through Object.setPrototypeOf()
) it will not have access to any of its normally inherited properties.
let reallyEmpty = Object.create(null);
console.log(reallyEmpty.toString()); // TypeError: not a function
In this particular case, __proto__
would also fail to work since it is inherited much like toString()
is.
let reallyEmpty = Object.create(null);
console.log(reallyEmpty.__proto__); // undefined (should be null)
let lessEmpty = { glassHalfFull: true };
reallyEmpty.__proto__ = lessEmpty; // (does not set prototype)
console.log(Object.getPrototypeOf(reallyEmpty)); // null
The actual prototype of an object is stored in an internal slot referred to as [[Prototype]]
which is not accessible directly. Some objects have a prototype
property, but it does not represent the prototype of its owner. The prototype
property exists on constructor functions (or any function that can be a constructor) and is the object used as the prototype for the instances that constructor creates.
let arr = new Array(); // arr prototype set to Array.prototype
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
For objects created with literal syntax, the same applies even though you’re not using new
explicitly. For example, {}
creates a new Object()
, []
creates a new Array()
, and /pattern/
creates a new RegExp(pattern)
.
let arr = []; // same as new Array()
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
So when we say an object inherits from the Object type, it’s inheriting from Object.prototype
.
let obj = {}; // same as new Object()
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
In creating your own constructor function, it is automatically given a prototype
property which is a normal object inheriting from Object.prototype
.
function Person () {} // (constructor function)
console.log(typeof Person.prototype); // object
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype);
// true
Using a constructor to create a new object creates a new object that inherits from the constructor’s prototype
which in turn inherits from Object.prototype
setting up the inheritance chain for that object.
function Person () {}
let me = new Person();
console.log(Object.getPrototypeOf(me) === Person.prototype); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype);
// true
console.log(me.toString()); // [object Object]
// (me's toString inherited from Person.prototype
// which inherited it from Object.prototype)
A constructor’s prototype
is where you’d store shared methods. Each instance created by the constructor would have access to the methods defined there because each instance inherits from the same prototype
object that’s defined in the constructor.
function Person () {}
Person.prototype.getAnswer = function () {
return 42;
}
let me = new Person();
console.log(me.getAnswer()); // 42
let myEvilClone = new Person();
console.log(friend.getAnswer()); // 42
The class
syntax allows you to do the same thing in a more concise manner. The result is the same: a constructor function that creates object’s that inherits from the class’s (constructor’s) prototype
object.
class Person {
getAnswer () { // defined on prototype
return 42;
}
}
console.log(typeof Person.prototype.getAnswer); // function
let me = new Person();
console.log(Object.getPrototypeOf(me) === Person.prototype); // true
console.log(me.getAnswer()); // 42
With classes, you can also easily extend other classes. This will add the extended class’s prototype
to the prototype chain of the instances it creates.
class Person {
getAnswer () {
return 42;
}
}
class SmartPerson extends Person {
getSmartAnswer () {
return 'Two score and two';
}
}
let me = new SmartPerson();
console.log(Object.getPrototypeOf(me) === SmartPerson.prototype);
// true
console.log(Object.getPrototypeOf(SmartPerson.prototype) === Person.prototype);
// true
console.log(me.getAnswer()); // 42 (from Person.prototype)
console.log(me.getSmartAnswer());
// Two score and two (from SmartPerson.prototype)
Classes are likely going to be the primary means by which you take advantage of prototypes in JavaScript. While you can use Object.setPrototypeOf()
to change an object’s prototype, it’s not common to do so and can even have a negative impact on performance. You generally want to create new objects with their prototype already in place and constructors created with class
are an easy and convenient way to do so.
While this has been a pretty quick and basic overview of prototypes in JavaScript, it can be a difficult concept to get your head wrapped around. If you don’t get it right away, that’s ok. It may take some time.
More info: