Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is there a need for prototype objects (in functions)?

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

Figure 1

However, when we start to use the actual inheritance constructions (with the new keyword) it starts to look like this:

Figure 2

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?

EDIT:

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?

like image 544
md2312 Avatar asked Dec 07 '20 12:12

md2312


2 Answers

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.

like image 172
Bergi Avatar answered Sep 28 '22 07:09

Bergi


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?

Prototypes are all about reuse

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.

like image 42
Benjamin Gruenbaum Avatar answered Sep 28 '22 08:09

Benjamin Gruenbaum