Rohit
Rohit

Reputation: 6613

How should the `.prototype` property be set up in a derived old-style constructor?

I am afraid to ask this question, as already there are so many on the same topic.

I am trying to understand cons/limitations of using approach-1 and approach-2

function Person(name, age) {
  this.name = name || "de-name";
  this.age = !!Number(age) ? Number(age) : 0;
}

Person.prototype.setName = function(name) {
  this.name = name;
  return this;
}

function Student(name) {
  Person.call(this, name); // Copy instance properties of Person class
  this.title = "Student";
}

// Suggested way
Student.prototype = Object.create(Person.prototype);


// Possible approach-1
// Student.prototype = Person.prototype;   

// Possible approach-2
// Student.prototype = new Person();

Upvotes: 2

Views: 86

Answers (4)

dumbass
dumbass

Reputation: 27201

In a derived old-style class, the .prototype property ought to be initialized by something similar to this:

Student.prototype = Object.create(
  Person.prototype, {
    'constructor': {
      configurable: true,
      enumerable: false,
      writable: true,
      value: Student,
    },
  });

Defining the .constructor property is optional, but if the base class contains it, then your derived class probably should as well, if only for the sake of avoiding being misled by its value when debugging. Going out of your way to set up the property descriptor like above is not that big of a deal either (simple assignment like Student.prototype.constructor = Student; will often be good enough in practice, even if it can be made to fail in some corner cases), but for consistency with the way .constructor is defined elsewhere in the language, it’s a good idea to perform nevertheless.

Alternatively, you can adjust the prototype of the pre-made derived instance prototype to point to the base instance prototype. Using ES6 features, one would do it like this:

Object.setPrototypeOf(Student.prototype, Person.prototype);

But this form is anachronistic – under actual pre-ES6 engines it would necessitate using then-not-yet-standard (and not-yet-behaving-like-in-the-standard) extensions, like __proto__:

Student.prototype.__proto__ = Person.prototype;

Either way gives you more or less the same set-up that modern, ES6-style classes make (and how transpilers like Babel implement ES6 classes in the first place), so it should create few surprises when mixing code and interoperating with those.

As for why not use other solutions, see below.

Student.prototype = Person.prototype;

This one is easy. It sets Student.prototype to be the very same object as Person.prototype, which means that:

  • properties defined in one will be added to the other – you will not be able add properties to Student instances that will not be available for Person instances;
  • x instanceof Person and x instanceof Student will return the same value – instances of Student will be indistinguishable from instances of Person.

This means Student would not behave like a derived class at all and will effectively act like an alternative constructor of the Person type: objects constructed from one will have the same prototype chain as the other. The only case when it might be justifiable to use something like this is if you decided to define a single type with multiple constructors acting as separate entry points:

function Student(name) {
  return Person.call(this, name);
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;     // sloppy job for brevity

Student.fromDbId = function (id) {
  var record = lookupRecordInDb(id);
  return Person.call(this, record.name);
}

Student.fromDbId.prototype = Student.prototype;

var s0 = new Student('Steve');
var s1 = new Student.fromDbId(12345);

console.log(s0 instanceof Student);
console.log(s1 instanceof Student);

Even then, a static factory function used without a new operator would be a more typical idiom.

Student.prototype = new Person();

This is also a bad idea, but for much more subtle reasons; ultimately, it is because a prototype is not supposed to be a fully-formed object, but only a collection of properties to be attached to one. This is a point not very easy to see in a bare-bones example of an inert Student deriving from an inert Person, so instead of that, imagine you wanted to derive from a constructor that performs some non-trivial work, like for a class that represents a network connection:

// this is the base class you want to use
function Socket(hostname, port) {
  // validate arguments
  if (hostname == null || port == null)
    throw new TypeError("invalid arguments");

  // connect to ⟨hostname⟩:⟨port⟩, then store
  // the connection handle/file descriptor/whatever somewhere
}

To create a Socket, you have to pass the address you want to connect to; otherwise construction will fail. Now suppose you want to create your own derived socket class with extra methods (admittedly, inheritance is probably not a great tool to achieve this, but then, what is inheritance ever good for, really?):

function MyAwesomeSocket(hostname, port) {
  return Socket.call(this, hostname, port);     // I guess?
}

MyAwesomeSocket.prototype = new Socket(/* what goes here? */);

If you choose the approach of using a base instance as the prototype of derived instances, you will have to construct a Socket here, and this Socket will have to connect to something, because that is what the constructor does. This connection will not be actually used for anything, though – it only exists for the sake of being able to access methods defined by the Socket class. This is, essentially, a resource leak. And it just does not make sense logically: you don’t want to create a connection, you only want the methods associated with one. It’s even worse when you consider the possibility that establishing this useless connection may fail. A Socket class simply cannot be derived from in this manner.

Upvotes: 1

mAAdhaTTah
mAAdhaTTah

Reputation: 400

Approach 1 and Approach 2 are actually not the same, exactly. In approach 2, you're creating a new instance of Person, and assigning that new instance to the prototype of Student.

Additionally, you're supposed to do it this way:

var Student = Object.create(Person.prototype);

According to MDN:

The Object.create() method creates a new object with the specified prototype object and properties.

So you don't assign it to the Student prototype, you assign it to the Student itself, and Student gets Person as its prototype.

Upvotes: 0

fny
fny

Reputation: 33517

In prototype-based languages, inheritance is performed by cloning existing objects that serve as prototypes rather than having classes.

So in each case, we should think about the object selected to use as the prototype to figure out the behavior.

In approach 1, you're setting the prototype of Student to the same prototype object as Person. This means that any changes made to Student.prototype will affect Person.prototype and vice versa.

In approach 2, you're setting the prototype of Student to a new Person object that will have the following properties set {name: 'de-name', age: 0} according to your initialization code. The name property will then be overriden by your call to Person.call() in the Student function. Since this an entirely new object, any modifications to Student.prototype will only affect new Student objects, and any missing properties on this Person instance that serves as the prototype will be delegated to the Person.prototype.

To elaborate on that last bit (that missing properties are passed up the prototype chain), here's an example. Say we add a new method greet to Person:

Person.prototype.greet = function() { console.log("Hi! " + this.name; ) }

Calling new Student().greet() will have JavaScript check through the prototype chain until it hits the appropriate property (otherwise you get a not defined error.)

// Walking up the chain
/* 1 */ new Student()     // doesn't have a greet property
/* 2 */ Student.prototype // => Person {name: 'de-name', age: 0}
// This person instance doesn't have a greet property either
// because we instantiated it before adding the `greet` property
/* 3 */ Person.prototype  // Has a greet property which gets called

In the suggested pattern with Object.create, you're doing almost the same thing as Student.prototype = new Person() except that Object.create allows you to perform differential inheritance. You can even add additional properties as its second argument:

Student.prototype = Object.create(Person.prototype, {
  age: 16,
  study: function() { console.log("But I'm sooo lazy!!!"); } 
});

Upvotes: 0

Petr Skocik
Petr Skocik

Reputation: 60056

approach-2 means one additional object in the prototype chain. If (new Student()).someAttr doesn't resolve in the student object (new Student()), with approach-2, the person object (new Person()) is checked (because that's what's in Student.prototype), then Person.prototype. With approach-1, there's no person object.

Upvotes: 0

Related Questions