Several months ago, I set out to get a deeper understanding of JavaScript, specifically in the area of prototypes. Prototypes were a fuzzy concept for me. I sort of knew how to use them, but didn't fully understand them. After a lot of study, I've started to see the picture of how prototypes work, and that they are not as complicated as I first thought.
What is a prototype?
Some confusion may arise when talking about prototypes because the word "prototype" may be used to describe the internal [[Prototype]]
property of an object, or the prototype
property of a function. These are two separate things, but later on we'll see that they relate.
Internal prototype
Strings, numbers, booleans, objects, arrays, and functions are created with an internal [[Prototype]]
property. The value of this property is an object, whose purpose purpose will be explained later. Since this property is internal, it is not exposed to us through the code we write. However, when the fifth version of EcmaScript came along, the specification did allow the retrieval of this internal property with Object.getPrototypeOf
.
Some time later, browsers started implementing a property called __proto__
, which allows both reading and writing of an object's internal [[Prototype]]
property. It's important to know that the use of this property is highly discouraged, and an alternative is explained later on. The __proto__
property is only used here in examples as it provides a better illustration.
Also, it's important to remember that both Object.getPrototypeOf
and the __proto__
property return the internal [[Prototype]]
of an object. So, when I say [[Prototype]]
, I'm referring to the object returned from Object.getPrototypeOf
or __proto__
.
var bob = {}
var str = 'Hello'
var num = 123
// They all have a [[Prototype]], which we can retrieve with __proto__
console.log(typeof bob.__proto__) // 'object'
console.log(typeof str.__proto__) // 'object'
console.log(typeof num.__proto__) // 'object'
// we can also retrieve it with Object.getPrototypeOf
console.log(Object.getPrototypeOf(bob) === bob.__proto__) // true
Function prototype
Functions are created with a property called prototype
, whose value is an object. Most people have probably found themselves using this property at some point. It is a common pattern to assign what are commonly called "methods", which are functions, or other values to this object, and then use the function as constructor, meaning the function is called with new
. When the function is called with new
, an object is returned that inherits the properties of the constructor's prototype
object. This is called the "constructor" pattern.
// Person is the constructor
function Person() {}
// The prototype property is a normal object
console.log(Person.prototype) // {}
Person.prototype.eatFood = function () {
console.log('scrumptious!')
}
var bob = new Person()
// bob is an object that "inherits" the eatFood function
bob.eatFood() // 'scrumptious!'
The eatFood
function was assigned to the Person.prototype
object, but we can access it on bob
. So, why does assigning values to the prototype
object allow us to then access those values on objects returned from the constructor? Well, there are two parts to this:
- What happens in the constructor when the object is created.
- How properties are resolved.
Constructor functions
A constructor, according to the EcmaScript spec, is a function that is meant for use with the new
operator. There's nothing special about them; they're just functions. Normally, the name of the function is written in Pascal casing, but this is just a convention programmers use to signify that the function is intended to be used with the new
operator.
When you call a function with the new
operator, the function runs as it normally would, except the value of this
is set to a newly created object, and the object created has its [[Prototype]]
set to the prototype
object of the constructor. After the code inside the function finishes, the newly created object is returned.
Below is an example of what happens in the background when a function is called with the new
operator. The code below wouldn't actually run because assigning a value to this
would generate a syntax error, but the code would otherwise run just fine.
function Person() {
// Set "this" to a new object
var this = {}
// Set the [[Prototype]] of the object to the constructor's prototype
this.__proto__ = Person.prototype
// The code defined in the function would run at this point
// return the object
return this
}
Person.prototype.eatFood = function () {
console.log('scrumptious!')
}
var bob = new Person()
bob.eatFood() // 'scrumptious!'
// Bob's [[Prototype]] is Person.prototyope
console.log(bob.__proto__ === Person.prototype) // true
Note: You can find this process in the ES5 spec here.
This setting of the newly created object's [[Prototype]]
to its constructor's protototype
object is the same thing that happens for strings, numbers, booleans, objects, arrays, and functions. They all have their [[Prototype]]
set to the prototype
property of their associated constructor.
// An object's [[Prototype]] is Object.prototype
console.log({}.__proto__ === Object.prototype) // true
// An array's [[Prototype]] is Array.prototype
console.log([].__proto__ === Array.prototype) // true
// A string's [[Prototype]] is String.prototype
console.log('Hello'.__proto__ === String.prototype) // true
This is how the [[Prototype]]
of an object relates to the prototype
of its constructor, they're the same object. Although this is true, this doesn't explain how we can set the function eatFood
on Person.prototype
, and be able to call it on bob
. This brings in the second part, which is property lookup.
Property lookup
When a property isn't found on the object in question, the property lookup goes from the object to its [[Prototype]]
object. From the earlier example of the Person
constructor, we can see that the constructor sets bob
's [[Prototype]]
to its prototype
object. Therefore, when accessing a property on bob
that bob
doesn't have, the property lookup will go to the Person.prototype
object.
If the Person.prototype
object didn't have the property, the property lookup would go to the [[Prototype]]
of the Person.prototype
object, which would be Object.prototype
. If Object.prototype
didn't have the property we would check its [[Prototype]]
. However, Object.prototype
has its [[Prototype]]
explicitly set to null, the purpose being that the property lookup is supposed to end there, since the spec says when a [[Prototype]]
that is null is reached, the property lookup ends and undefined
should be returned in place of the property's value.
Note: If you're interested, you can read about the property lookup operations in the ES5 spec here.
The prototype chain of bob
Person.prototype Object.prototype
| |
bob -> bob.__proto__ -> bob.__proto__.__proto__
So, the purpose of the [[Prototype]]
is as a delegate for property lookup in the case the original object doesn't have the property. In essence, it's another place to look for the properties of an object.
An important thing to realize here is that the property lookup doesn't go from the object, to its constructor, and then to the constructor's prototype
object. The property lookup goes from the object to whatever is set as its [[Prototype]]
. We could at anytime change the value of Person
's prototype
property to something else, and bob
's [[Prototype]]
would still point to the constructor's original prototype
object, because that's what bob
was initialized with.
function Person() {}
var originalPrototype = Person.prototype
// Bob has its linking to the original prototype
var bob = new Person()
Person.prototype = {}
// Bob doesn't link to Person's new prototype
console.log(bob.__proto__ === Person.prototype) // false
// Bob's `[[Prototype]]` is still the original Person.prototype object
console.log(bob.__proto__ === originalPrototype) // true
This is why you'll hear sayings such as "objects link to other objects". Objects are directly linked together through their [[Prototype]]
, meaning the constructor is actually extraneous. bob
links directly to another object, that object being Person.prototype
, and Person.prototype
links directly to another object, that object being Object.prototype
.
Now, knowing how property lookup works, we should be able to see this property lookup delegation at work if we were to put a property on Object.prototype
. Since mostly all objects have a prototype chain that leads back to Object.prototype
, we should be to access it on to strings, numbers, objects, arrays, etc.
Object.prototype.coolStuff = function () {
console.log('Really cool')
}
;({}.coolStuff()) // 'really cool'
;[].coolStuff() // 'really cool'
''.coolStuff() // 'really cool'
;(123).coolStuff() // 'really cool'
function eatFood() {}
eatFood.coolStuff() // 'really cool'
Object.prototype.coolStuff.coolStuff() // 'really cool'
The last one, where we call coolStuff
as a property of itself, works because coolStuff
has its [[Prototype]]
set to Function.prototype
. Since Function.prototype
doesn't have a property with the name of coolStuff
, the property lookup goes to the [[Prototype]]
of Function.prototype
, which is Object.prototype
. Since Object.prototype
has a property named coolStuff
, the value for the property is returned.
The prototype chain of coolStuff
Function.prototype Object.prototype
| |
coolStuff.__proto__ -> coolStuff.__proto__.__proto__
As another fun example, we'll set a property on Object.prototype
that has a number as the key, and then access the same key on an array.
var arr = []
Object.prototype[0] = 'tomatoes'
console.log(arr[0])
var names = ['John', 'Bob', 'Jim']
console.log(names[0])
What do you think will be logged to the console with arr[0]
? If your answer is tomatoes
, you'd be correct! Array indices are properties, and resolve in the same way properties do. The property lookup goes from the array, to Array.prototype
, and then to Object.prototype
.
What do you think will be logged to the console with names[0]
? If you guessed John
, you'd also be correct. Since the names
array has a property with the name of 0
, there is no need to check the prototype chain.
Prototypes without constructors
The examples before show objects having a [[Prototype]]
such as String.prototype
and Person.prototype
, where String
and Person
are constructor functions. But, as was mentioned before, constructors are extraneous and aren't actually necessary for inheritance to take place. The only parts necessary are two objects, and all we have to do is set one object as the [[Prototype]]
of the other object.
var bob = {}
var person = {
eatFood: function () {
console.log('scrumptious!')
},
}
// Sets person as the [[Prototype]] of bob
bob.__proto__ = person
// bob can still eat his food
bob.eatFood() // 'scrumptious!'
In the above example, person
is set as the [[Prototype]]
of bob
, meaning bob
will now delegate property access to person
when bob
itself doesn't have the property.
Note: Although this would produce the intended result, assigning to the __proto__
property in this way is highly discouraged. The safe way to set the [[Prototype]]
of an object is with Object.create
.
Object.create
Object.create
is a function that simply creates an object and sets the object's [[Prototype]]
to what is passed as the first argument. It also allows defining new properties on the object, but we won't focus on that here.
var person = {
eatFood: function () {
console.log('scrumptious!')
},
}
var bob = Object.create(person)
// Bob's [[Prototype]] is set to person
console.log(bob.__proto__ === person) // true
bob.eatFood() // 'scrumptious!'
With this comes the prototypal design pattern. Some may say this pattern "creates objects from other objects", but in my opinion, this isn't really accurate, since what's happening is we're just creating a new, arbitrary object, and settings its [[Prototype]]
to another object. There isn't really a creation from another object. There isn't a copying of another object. There's just an object, which delegates property lookup to its [[Prototype]]
object, and so on through its prototype chain.
// The functionality representing a person
var person = {
eatFood: function () {
console.log('scrumptious!');
}
};
// The functionality representing a salesman
var salesman = Object.create(person);
// We then extend salesman to have the functionality it needs
salesman.sellThings = function () {
console.log('I am selling things');
}
// We can then create objects that have the functionality of a person and salesman
var bob = Object.create(salesman);
bob.sellThings(); // 'I am selling things'
bob.eatFood(); // 'scrumptious!'
// We can then set properties onto bob as needed
bob.firstName = 'bob';
// The prototype chain of bob looks like this:
salesman person Object.prototype
| | |
bob.__proto__.__proto__.__proto__
Comparing Object.create and constructors
If we compare the simplified illustration of what happens in the background of a constructor call and a simplified version of Object.create
, we would see some similarities.
function Person() {
// Create a new object
var this = {}
// Set the object's [[Prototype]]
this.__proto__ = Person.prototype
// The code in the function would run at this point
// Return the object
return this
}
Object.create = function (prototype) {
// Create a new object
var obj = {}
// Set the object's [[Prototype]]
obj.__proto__ = prototype
// Return the object
return obj
}
They both create a new object, and then set the [[Prototype]]
of the new object to another object, which sets up the environment to allow property delegation to work. So, constructors are more like factories, returning objects pre-linked to another object, and the linking just happens to be to the constructor's prototype
object.
Now, you may say, "That's not the Object.create polyfill I've seen elsewhere!". That's true, it's not. If we were to look at a polyfill for Object.create
we would see the following.
Object.create = function (proto, properties) {
if (typeof proto !== 'object' && typeof proto !== null) {
throw new Error('Object prototype may only be an Object or null')
}
function Temp() {}
Temp.prototype = proto
// Create the new object
var obj = new Temp()
ObjectDefineProperties(obj, properties)
return obj
}
The reason why a constructor is used in the actual polyfill instead of __proto__
is because assigning to __proto__
brings performance hits, and may not entirely be cross browser. The only way to create an object with a specific [[Prototype]]
in older browsers is with a constructor function.
If we look at the definition of Object.create
in NodeJS, we would see that it doesn't use a constructor function (code obtained from the NodeJS github repository).
// ES5 section 15.2.3.5.
function ObjectCreate(proto, properties) {
if (!IS_SPEC_OBJECT(proto) && proto !== null) {
throw MakeTypeError(kProtoObjectOrNull, proto)
}
var obj = {}
%InternalSetPrototype(obj, proto)
if (!IS_UNDEFINED(properties)) ObjectDefineProperties(obj, properties)
return obj
}
There's no constructor function here. It literally just creates a new, empty object, and sets the newly created object's [[Prototype]]
with some sort of internal function.
Creating objects without prototypes
When Object.create
is passed null
, the object returned will have no object set as its [[Prototype]]
. I have yet to find a use case for this, but it's interesting nonetheless.
var bob = Object.create(null)
console.log(Object.getPrototypeOf(bob)) // null
bob.toString() // TypeError: bob.toString is not a function
If we try to call the toString
property of bob
, we'll get a TypeError saying "bob.toString is not a function". This is because the toString
property is defined on Object.prototype
, but since bob
has a [[Prototype]]
that is null
, the property lookup has nowhere to go, which means it can't reach Object.prototype
. As a result, the property lookup returns undefined
, which of course is not callable, hence "bob.toString is not a function".
Constructors, classes, and prototypal patterns
It doesn't matter which pattern you choose, whether it be the constructor pattern, classes, the prototypal pattern, or any other pattern that claims "inheritance" in JavaScript. They all use the same underlying mechanism, which is the delegation of property lookup through an object's prototype chain.
Primitive prototypes
I have referred to strings, numbers, and booleans as objects, and that we can access properties on them, but this is not technically true. Strings, numbers, and booleans are primitives, and they themselves don't have properties. When accessing a property on a value that is a primitive, the JS engine wraps the primitive in an object wrapper. For example, a number would be wrapped in a Number object wrapper, and a string would be wrapped in a String object wrapper. This process is called "boxing and unboxing".
So, when I call numbers, strings, and booleans objects, I'm referring to their object wrappers. The wrappers are the ones that we are accessing properties on, not the primitive itself.
Note: The function that wraps primitives in their object wrappers can be found here.
// so this
'hello'.__proto__
// is turned into this by the JS engine automatically
new String('hello').__proto__
console.log(new String('hello'))
// {
// 0: "h",
// 1: "e",
// 2: "l",
// 3: "l",
// 4: "o",
// length: 5,
// [[PrimitiveValue]]: "hello"
// }
console.log(new Number(10)) // { [[PrimitiveValue]]: 12 }
Conclusion
Prototypes don't have to be magic. Once you learn how they work, it will explain a lot of the patterns that are out there. If you still don't understand how prototypes after reading this, that's normal. It took me a while before I finally understood them. Once you learn them though, you'll realize the concept is actually really simple! Keep on learning and you'll get them down!