I have been trying to learn about prototypes since I needed quite a bit of clarity on them. I have been using this MDN article and its related articles for reference.
After some reading, I got a bit of clarity on prototypes and tried putting them in my own words and would like to know if it is right and would love to know if and where I am wrong
START OF EXPLANATION
Every object is created using a constructor function of some sort. Say, we create an object as follows.
let obj = new Object();
Here, Object
is the constructor function. The thing about functions is that, all of them (including non-constructor functions) have a property called prototype
on them. This prototype
property defines what will be the prototype of any object that is created using the new
keyword and said constructor function. You can check the prototype property as follows:
console.log(Object.prototype);
The above piece of code will return an object with a bunch of methods that any object created using new Object()
can use.
In the above example, if the wording is too confusing, you can replace all occurrences of Object
with any other constructor function such as Array
, Date
or even custom constructor functions such as Person
or something else that you may have defined.
END OF EXPLANATION
Is my understanding right? If not, can you point me where I went wrong?
Is my understanding right? If not, can you point me where I went wrong?
In big picture terms, yes, your understanding is mostly correct, but the explanation is incomplete, and there are some specifics that are incorrect.
Every object is created using a constructor function of some sort.
That's not quite correct, JavaScript also has literal forms ({}
[object], []
[array], and //
[regular expression]) that create objects without using a constructor function. Those forms assign Object.prototype
, Array.prototype
, and RegExp.prototype
(respectively) to the objects they create, even though the constructor itself isn't invoked.
There are also other ways of creating objects that don't go through constructor functions at all. For instance, there's Object.create
, which creates an object and assigns the prototype you supply to it:
const p = {};
const obj = Object.create(p);
console.log(Object.getPrototypeOf(obj) === p); // true
(There are also more obscure ways of creating objects through implicit conversion.) You can also change the prototype of an existing object by using Object.setPrototypeOf
.
The thing about functions is that, all of them (including non-constructor functions) have a property called
prototype
on them.
Not quite, arrow functions and class methods do not have a prototype
property and cannot be used as constructors:
const arrow = () => {};
class X {
method() {
}
static staticMethod() {
}
}
console.log("prototype" in arrow); // false
console.log("prototype" in X.prototype.method); // false
console.log("prototype" in X.staticMethod); // false
This
prototype
property defines what will be the prototype of any object that is created using thenew
keyword and said constructor function.
Correct. (Constructor functions can mess with what they return, but that's the usual, standard behavior.)
At this point in an explanation I'd probably point out the distinction between the prototype
property on functions and the prototype of an object. Beginners sometimes think setting a prototype
property on an object will change its prototype; it doesn't, that name is only significant on functions, and it's not the function's prototype, it's just a property that (as you said) will be used to assign the prototype of an object created using new
with that function. The prototype of an object is held in an internal field of the object called [[Prototype]]
. That field isn't directly accessible, but you can access it via Object.getPrototypeOf
and change it via Object.setPrototypeOf
(you can also use the deprecated __proto__
accessor property, which is just a wrapper around those functions — but don't use __proto__
, use the functions directly).
But aside from all that, there's a big unanswered question in your explanation: What are prototypes for? What do they do? Why have them?
The answer is that they provide JavaScript's inheritance mechanism. When you get the value of a property on an object and the object doesn't have a property of its own with the given key, the JavaScript engine looks at the object's prototype to see if it has the property (and the prototype of the prototype, and so on through the chain):
const parent = {
a: "a property on base",
};
const child = Object.create(parent);
child.b = "a property on child";
const grandChild = Object.create(child);
grandChild.c = "a property on grandChild";
console.log(grandChild.a); // "a property on base"
console.log(grandChild.b); // "a property on child"
console.log(grandChild.c); // "a property on grandChild"
const hasOwn =
Object.hasOwn || // Fairly new, ES2022
Function.prototype.call.bind(Object.prototype.hasOwnProperty);
console.log(`hasOwn(grandChild, "a")? ${hasOwn(grandChild, "a")}`); // false
console.log(`hasOwn(grandChild, "b")? ${hasOwn(grandChild, "b")}`); // false
console.log(`hasOwn(grandChild, "c")? ${hasOwn(grandChild, "c")}`); // true
Those example property values are strings, but this is widely used where the property values are functions, providing a means of inheriting methods from parent objects.
The property access process is asymmetrical, though; it only works as described above for getting a property's value. If you set a property's value on an object, it always sets it on the object itself, not on its prototype:
const parent = {
prop: "parent",
};
const child = Object.create(parent);
const hasOwn =
Object.hasOwn || // Fairly new, ES2022
Function.prototype.call.bind(Object.prototype.hasOwnProperty);
console.log(`[Before] child.prop: ${child.prop}`);
// => "[Before] child.prop: parent"
console.log(`[Before] hasOwn(child, "prop")? ${hasOwn(child, "prop")}`);
// => "[Before] hasOwn(child, "prop")? false"
child.prop = "child";
console.log(`[After] child.prop: ${child.prop}`);
// => "child.prop: child"
console.log(`[After] hasOwn(child, "prop")? ${hasOwn(child, "prop")}`);
// => "[After] hasOwn(child, "prop")? true"
(This difference between getting and setting the property value applies to data properties [the kind we mostly create]; accessor properties work differently because getting and setting the property result in function calls, and the accessor's setter function can do whatever the author wants it to do.)