I've read a ton of materials about prototypes and understand inheritance in general. However, this is one thing that is bugging me and I cannot figure it out.
On dmitrysoshnikov.com there is a simplified example of how prototypal inheritance could be achieved with the following snippet:
// Generic prototype for all letters.
let letter = {
getNumber() {
return this.number;
}
};
let a = {number: 1, __proto__: letter};
let b = {number: 2, __proto__: letter};
// ...
let z = {number: 26, __proto__: letter};
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);
Which follows with this diagram
However, when we start to use the actual inheritance constructions (with the new keyword) it starts to look like this:
I understand how it works. What I don't understand is why suddenly we need the Letter.prototype object from which all the child instances inherit from instead of having it like the first diagram above. To me, it didn't seem like anything was broken with the first example.
One potential reason I can think of is that the actual way allows implementing static methods/properties in classes. In the example above if you'd add a static method then it would be a function added to the Letter object but not Letter.prototype object. The child objects (a,b,z) would have no access to that function. In the first example, this kind of feature would have to be implemented differently but I still don't think that's a good enough reason to create the new Prototype object. I think this static methods feature could be implemented without it.
Am I missing something?
I think there is a lot of people trying to explain things which I'm grateful but I'm not sure my question of WHY was javascript runtime designed to behave one way instead of another properly understood.
To show what I mean here is a few things I tried.
class Car{
method() {
console.log("hello")
}
}
myCar = new Car();
// First a few tests as expected
myCar.method() // works
console.log(myCar.method === Car.method) // False, JS doesn't work that way, ok...
console.log(myCar.method === Car.prototype.method) // This is how it works, fine...
// How about we move the reference to the method up one level
Car.method = Car.prototype.method
// Delete the reference to it in prototype object,
// Btw. I tried to remove reference to whole prototype but somehow doesn't let me
delete Car.prototype.method
// Change the prototype chain so it links directly to Car and not Car's prototype object
myCar.__proto__ = Car
myCar.method() // Still works!!!
console.log(myCar.method === Car.method) // True !
console.log(myCar.method === Car.prototype.method) // False, we deleted the method property out of Car.prototype
So, Car.prototype
is not needed anymore, at least not for myCar's execution.
So why does the method go inside Car.prototype
and not Car
and then why not myCar.__proto__ = Car
instead of myCar.__proto__ = Car.prototype
?
I don't understand is why suddenly we need the
Letter.prototype
object from which all the child instances inherit from instead of having it like the first diagram above.
Actually nothing changed there. It's still the same object with the same purpose as the object named const letter
in the first example. The letter instances inherit from it, it stores the getNumber
method, it inherits from Object.prototype
.
What changed is the additional Letter
function.
To me it didn't seem like anything was broken with the first example.
Yes, it was: {number: 2, __proto__: letter}
is a really ugly way of creating an instance, and doesn't work when having to execute more complicated logic to initialise the properties.
An approach to fix this issue is
// Generic prototype for all letters.
const letterPrototype = {
getNumber() {
return this.number;
}
};
const makeLetter = (number) => {
const letter = Object.create(letterPrototype); // {__proto__: letterPrototype}
if (number < 0) throw new RangeError("letters must be numbered positive"); // or something
letter.number = number;
return letter;
}
let a = makeLetter(1);
let b = makeLetter(2);
// ...
let z = makeLetter(26);
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);
Now we have two values, makeLetter
and letterPrototype
that somehow belong to each other. Also, when comparing all kinds of make…
functions, they all share the same pattern of first creating a new object inheriting from the respective prototype, then returning it at the end. To simplify, a generic construct was introduced:
// generic object instantiation
const makeNew = (prototype, ...args) => {
const obj = Object.create(prototype);
obj.constructor(...args);
return obj;
}
// prototype for all letters.
const letter = {
constructor(number) {
if (number < 0) throw new RangeError("letters must be numbered positive"); // or something
letter.number = number;
},
getNumber() {
return this.number;
}
};
let a = makeNew(letter, 1);
let b = makeNew(letter, 2);
// ...
let z = makeNew(letter, 26);
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);
You can see where we're going? makeNew
is actually part of the language, the new
operator. While this would have worked, what was actually chosen for the syntax is to make the constructor
the value being passed to new
and the prototype object being stored on .prototype
of the constructor function.
To me it didn't seem like anything was broken with the first example.
It's not (objectively) and certain people (like Douglas Crockford) have often advocated to avoid .prototype
and this
all together and use Object.create
(similar to your __proto__
example).
So why do people prefer to use classes, inheritance and .prototype?
The reason you typically prototypes is to reuse functionality (like getNumber
above). In order to do that it's convenient to use a constructor.
A constructor is just a function that creates objects. In "old" JavaScript you would do:
function Foo(x) { // easy, lets me create many Xs
this.x = x;
}
// easy, shares functionality across all objects created with new Foo
Foo.prototype.printX() {
console.log(this.x);
}
// note that printX isn't saved on every object instance but only once
(new Foo(4)).printX();
ES2015 made this even easier:
class Foo { // roughly sugar for the above with subtle differences.
constructor(x) { this.x = x; }
printX() { console.log(this.x); }
}
So to sum it up: you don't have to use .prototype and classes, people do so because it is useful. Note the prototype chain is as large in both examples.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With