Why is it necessary to set the prototype constructor?

60,129

Solution 1

It's not always necessary, but it does have its uses. Suppose we wanted to make a copy method on the base Person class. Like this:

// define the Person Class  
function Person(name) {
    this.name = name;
}  

Person.prototype.copy = function() {  
    // return new Person(this.name); // just as bad
    return new this.constructor(this.name);
};  

// define the Student class  
function Student(name) {  
    Person.call(this, name);
}  

// inherit Person  
Student.prototype = Object.create(Person.prototype);

Now what happens when we create a new Student and copy it?

var student1 = new Student("trinth");  
console.log(student1.copy() instanceof Student); // => false

The copy is not an instance of Student. This is because (without explicit checks), we'd have no way to return a Student copy from the "base" class. We can only return a Person. However, if we had reset the constructor:

// correct the constructor pointer because it points to Person  
Student.prototype.constructor = Student;

...then everything works as expected:

var student1 = new Student("trinth");  
console.log(student1.copy() instanceof Student); // => true

Solution 2

Does this serve any important purpose?

Yes and no.

In ES5 and earlier, JavaScript itself didn't use constructor for anything. It defined that the default object on a function's prototype property would have it and that it would refer back to the function, and that was it. Nothing else in the specification referred to it at all.

That changed in ES2015 (ES6), which started using it in relation to inheritance hierarchies. For instance, Promise#then uses the constructor property of the promise you call it on (via SpeciesConstructor) when building the new promise to return. It's also involved in subtyping arrays (via ArraySpeciesCreate).

Outside of the language itself, sometimes people would use it when trying to build generic "clone" functions or just generally when they wanted to refer to what they believed would be the object's constructor function. My experience is that using it is rare, but sometimes people do use it.

Is it okay to omit it?

It's there by default, you only need to put it back when you replace the object on a function's prototype property:

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

If you don't do this:

Student.prototype.constructor = Student;

...then Student.prototype.constructor inherits from Person.prototype which (presumably) has constructor = Person. So it's misleading. And of course, if you're subclassing something that uses it (like Promise or Array) and not using class¹ (which handles this for you), you'll want to make sure you set it correctly. So basically: It's a good idea.

It's okay if nothing in your code (or library code you use) uses it. I've always ensured it was correctly wired up.

Of course, with ES2015 (aka ES6)'s class keyword, most of the time we would have used it, we don't have to anymore, because it's handled for us when we do

class Student extends Person {
}

¹ "...if you're subclassing something that uses it (like Promise or Array) and not using class..." — It's possible to do that, but it's a real pain (and a bit silly). You have to use Reflect.construct.

Solution 3

TLDR; Not super necessary, but will probably help in the long run, and it is more accurate to do so.

NOTE: Much edited as my previous answer was confusingly written and had some errors that I missed in my rush to answer. Thanks to those who pointed out some egregious errors.

Basically, it's to wire subclassing up correctly in Javascript. When we subclass, we have to do some funky things to make sure that the prototypal delegation works correctly, including overwriting a prototype object. Overwriting a prototype object includes the constructor, so we then need to fix the reference.

Let's quickly go through how 'classes' in ES5 work.

Let's say you have a constructor function and its prototype:

//Constructor Function
var Person = function(name, age) {
  this.name = name;
  this.age = age;
}

//Prototype Object - shared between all instances of Person
Person.prototype = {
  species: 'human',
}

When you call the constructor to instantiate, say Adam:

// instantiate using the 'new' keyword
var adam = new Person('Adam', 19);

The new keyword invoked with 'Person' basically will run the Person constructor with a few additional lines of code:

function Person (name, age) {
  // This additional line is automatically added by the keyword 'new'
  // it sets up the relationship between the instance and the prototype object
  // So that the instance will delegate to the Prototype object
  this = Object.create(Person.prototype);

  this.name = name;
  this.age = age;

  return this;
}

/* So 'adam' will be an object that looks like this:
 * {
 *   name: 'Adam',
 *   age: 19
 * }
 */

If we console.log(adam.species), the lookup will fail at the adam instance, and look up the prototypal chain to its .prototype, which is Person.prototype - and Person.prototype has a .species property, so the lookup will succeed at Person.prototype. It will then log 'human'.

Here, the Person.prototype.constructor will correctly point to Person.

So now the interesting part, the so-called 'subclassing'. If we want to create a Student class, that is a subclass of the Person class with some additional changes, we'll need to make sure that the Student.prototype.constructor points to Student for accuracy.

It doesn't do this by itself. When you subclass, the code looks like this:

var Student = function(name, age, school) {
 // Calls the 'super' class, as every student is an instance of a Person
 Person.call(this, name, age);
 // This is what makes the Student instances different
 this.school = school
}

var eve = new Student('Eve', 20, 'UCSF');

console.log(Student.prototype); // this will be an empty object: {}

Calling new Student() here would return an object with all of the properties we want. Here, if we check eve instanceof Person, it would return false. If we try to access eve.species, it would return undefined.

In other words, we need to wire up the delegation so that eve instanceof Person returns true and so that instances of Student delegate correctly to Student.prototype, and then Person.prototype.

BUT since we're calling it with the new keyword, remember what that invocation adds? It would call Object.create(Student.prototype), which is how we set up that delegational relationship between Student and Student.prototype. Note that right now, Student.prototype is empty. So looking up .species an instance of Student would fail as it delegates to only Student.prototype, and the .species property doesn't exist on Student.prototype.

When we do assign Student.prototype to Object.create(Person.prototype), Student.prototype itself then delegates to Person.prototype, and looking up eve.species will return human as we expect. Presumably we would want it to inherit from Student.prototype AND Person.prototype. So we need to fix all of that.

/* This sets up the prototypal delegation correctly 
 *so that if a lookup fails on Student.prototype, it would delegate to Person's .prototype
 *This also allows us to add more things to Student.prototype 
 *that Person.prototype may not have
 *So now a failed lookup on an instance of Student 
 *will first look at Student.prototype, 
 *and failing that, go to Person.prototype (and failing /that/, where do we think it'll go?)
*/
Student.prototype = Object.create(Person.prototype);

Now the delegation works, but we're overwriting Student.prototype with an of Person.prototype. So if we call Student.prototype.constructor, it would point to Person instead of Student. This is why we need to fix it.

// Now we fix what the .constructor property is pointing to    
Student.prototype.constructor = Student

// If we check instanceof here
console.log(eve instanceof Person) // true

In ES5, our constructor property is a reference that refers to a function that we've written with the intent to be a 'constructor'. Aside from what the new keyword gives us, the constructor is otherwise a 'plain' function.

In ES6, the constructor is now built into the way we write classes - as in, it's provided as a method when we declare a class. This is simply syntactic sugar but it does accord us some conveniences like access to a super when we are extending an existing class. So we would write the above code like this:

class Person {
  // constructor function here
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  // static getter instead of a static property
  static get species() {
    return 'human';
  }
}

class Student extends Person {
   constructor(name, age, school) {
      // calling the superclass constructor
      super(name, age);
      this.school = school;
   }
}

Solution 4

I'd disagree. It isn't necessary to set the prototype. Take that exact same code but remove the prototype.constructor line. Does anything change? No. Now, make the following changes:

Person = function () {
    this.favoriteColor = 'black';
}

Student = function () {
    Person.call(this);
    this.favoriteColor = 'blue';
}

and at the end of the test code...

alert(student1.favoriteColor);

The color will be blue.

A change to the prototype.constructor, in my experience, doesn't do much unless you're doing very specific, very complicated things that probably aren't good practice anyway :)

Edit: After poking around the web for a bit and doing some experimentation, it looks like people set the constructor so that it 'looks' like the thing that is being constructed with 'new'. I guess I would argue that the problem with this is that javascript is a prototype language - there is no such thing as inheritence. But most programmers come from a background of programming that pushes inheritence as 'the way'. So we come up with all sorts of things to try and make this prototypical language a 'classic' language.. such as extending 'classes'. Really, in the example they gave, a new student is a person - it isn't 'extending' from another student.. the student is all about the person, and whatever the person is the student is as well. Extend the student, and whatever you've extended is a student at heart, but is customized to fit your needs.

Crockford is a bit crazy and overzealous, but do some serious reading on some of the stuff that he's written.. it'll make you look at this stuff very differently.

Solution 5

This has the huge pitfall that if you wrote

Student.prototype.constructor = Student;

but then if there was a Teacher whose prototype was also Person and you wrote

Teacher.prototype.constructor = Teacher;

then the Student constructor is now Teacher!

Edit: You can avoid this by ensuring that you had set the Student and Teacher prototypes using new instances of the Person class created using Object.create, as in the Mozilla example.

Student.prototype = Object.create(Person.prototype);
Teacher.prototype = Object.create(Person.prototype);
Share:
60,129
trinth
Author by

trinth

Updated on August 06, 2022

Comments

  • trinth
    trinth almost 2 years

    In the section about inheritance in the MDN article Introduction to Object Oriented Javascript, I noticed they set the prototype.constructor:

    // correct the constructor pointer because it points to Person
    Student.prototype.constructor = Student;  
    

    Does this serve any important purpose? Is it okay to omit it?

    • Wylie
      Wylie over 12 years
      Glad you asked this: I read the same documentation yesterday and was curious about the reasoning behind explicitly setting the constructor as well.
    • Marie
      Marie almost 9 years
      I just had to point this out, this question is now linked in the article you linked!
    • nothingisnecessary
      nothingisnecessary over 8 years
      nothing is necessary
    • ckybonist
      ckybonist about 5 years
      The subclass.prototype.constructor will point to parent_class if you don't write subclass.prototype.constructor = subclass; That is, using subclass.prototype.constructor() directly will produce an unexpected result.
    • Soner from The Ottoman Empire
      Soner from The Ottoman Empire almost 4 years
      @KuanYuChu what kind of unexpected result? I really wonder.
  • Stephen
    Stephen over 12 years
    The prototype constructure may become a person, but that is appropriate as it is inheriting all properties and methods from the Person. Creating a new Student() without setting the prototype.constructor appropriately calls its own constructor.
  • trinth
    trinth over 12 years
    Thanks, I did notice that omitting it did not affect my code, though it looks like it can come in handy sometimes.
  • Ansel Halliburton
    Ansel Halliburton over 12 years
    Note: The constructor attribute does not have any special meaning in JS, so you might as well call it bananashake. The only difference is that the engine automatically initializes constructor on f.prototype whenever you declare a function f. However, it can be overwritten any time.
  • Wayne
    Wayne over 12 years
    @Pumbaa80 - I get your point, but the fact that the engine automatically initializes constructor means that it does have special meaning in JS, pretty much by definition.
  • Ansel Halliburton
    Ansel Halliburton over 12 years
    Yea, I was not very clear on that. I was trying to say that you can't assign a constructor to a "class" in order to change the behaviour of new, but you have to do it the other way round: Assign a prototype to the function. Having the circular reference by setting constructor may be a nice idea, but fails when two functions point to the same prototype.
  • CEGRD
    CEGRD over 11 years
    I just want to clarify that the reason why the behavior you said works is because you use return new this.constructor(this.name); instead of return new Person(this.name);. Since this.constructor is the Student function (because you set it with Student.prototype.constructor = Student;), the copy function ends up calling the Student function. I am not sure what your intention was with the //just as bad comment.
  • CEGRD
    CEGRD over 11 years
    @lwburk what do you mean by "//just as bad"?
  • Wayne
    Wayne over 11 years
    @CEGRD - I mean that using that method will also return a Person object where instead we should expect the more specific Student
  • snapfractalpop
    snapfractalpop over 10 years
    I think I get it. But, what if the Student constructor had added an additional argument like: Student(name, id)? Do we then have to override the copy function, calling the Person version from within it, and then also copying the additional id property?
  • Wayne
    Wayne over 10 years
    Well, something like that. I'd probably just create a whole new copy method that copies both properties. I don't see any need to call the "parent" method.
  • binki
    binki about 9 years
    Setting constructor has just as many problems as directly calling new Person(). As @CEGRD said, the extending object’s function may be incompatible. Though, using constructor like this as a shorthand can be justified if it happens to work in most cases as it reduces code duplication. But it's being used as a way for types to opt-in to a specific clone implementation, not as a thing in and of itself (other than that JavaScript happens to set it sometimes…).
  • Bergi
    Bergi about 9 years
    Your createNewCar method is creating factories!? Also this looks like it should have been used as var audiFactory = new CarFactory("Audi") rather than using inheritance.
  • Roumelis George
    Roumelis George about 9 years
    You could just return Object.create(this) in the copy method
  • html_programmer
    html_programmer almost 9 years
    Wow, such simple way to achieve the concept of inheritance. I like the fact that it keeps the constructor property tied to the constructor function, which I found isn't always the case when applying other techniques. jsfiddle.net/2rmnce3g
  • Cypher
    Cypher almost 9 years
    This does not inherit the prototype chain.
  • Stephen
    Stephen almost 9 years
    @Cypher slow clap welcome to the conversation, four years later. Yeah, the prototype chain is inherited, regardless of whether you overwrite the prototype.constructor. Try testing it out.
  • Cypher
    Cypher over 8 years
    You're missing the code that inherits the prototype. Welcome to the internet.
  • Stephen
    Stephen over 8 years
    @Cypher Code snippet was based on the code in the linked article. Welcome to reading the question in its entirety. Oh. Wait.
  • schirrmacher
    schirrmacher over 8 years
    "there is no such thing as inheritence"??? Of course JavaScript uses prototypical inheritance! The only difference between JavaScript and "normal" inheritance languages is that it omits the abstraction of a class and you have to inherit directly from objects.
  • schirrmacher
    schirrmacher over 8 years
    @RoumelisGeorge Yes, but Object.create can't take arguments
  • Stephen
    Stephen over 8 years
    @macher I meant it as classical inheritance. Poor choice of wording on my part.
  • lucky
    lucky over 8 years
    Student.prototype = Object.create(...) is assumed in this question. This answer adds nothing but possible confusion.
  • Alex Ross
    Alex Ross over 8 years
    @AndréNeves I found this answer helpful. Object.create(...) is used in the MDN article that spawned the question, but not in the question itself. I'm sure many people don't click through.
  • Aseem Bansal
    Aseem Bansal over 8 years
    eve instanceof Student returned true. See stackoverflow.com/questions/35537995/… for explanation. Also when you say which is, at the moment, nothing what are you referring to? Every function has a prototype so if I check Student.prototype it is something.
  • bthehuman
    bthehuman over 8 years
    My mistake. It should have read 'eve instanceof Person' which would return false. I will amend that part. You are correct that every function has a prototype property. However, without assigning the prototype to Object.create(Person.prototype), the Student.prototype is empty. So if we log eve.species, it will not delegate properly up to its superclass, Person, and it will not log 'human'. Presumably, we want every subclass to inherit from its prototype and also its super's prototype.
  • bthehuman
    bthehuman over 8 years
    To clarify, by which is, at the moment, nothing, I meant that the Student.prototype object is empty.
  • bthehuman
    bthehuman over 8 years
    More on the prototype: Without the assignment of Student.prototype to Object.create(Person.prototype) - which is, if you recall, the same way all instances of Person are set up to delegate to Person.prototype - looking up a property on an instance of Student would delegate to only Student.prototype. So eve.species will fail its lookup. If we do assign it, Student.prototype itself then delegates to Person.prototype, and looking up eve.species will return human.
  • Felix Kling
    Felix Kling over 8 years
    It seems there are quite a few things wrong here: "It's necessary when you're attempting to emulate 'subclassing' [...] so that when you check if an instance is instance the 'subclass' Constructor, it will be accurate." Nope, instanceof doesn't use constructor. "However, if we look up the student's .prototype.constructor, it would still point to Person" Nope, it will be Student. I don't understand the point of this example. Calling a function in a constructor is not inheritance. "In ES6, the constructor is now an actual function instead of a reference to a function" Uh what?
  • bthehuman
    bthehuman over 8 years
    Hey Felix - you're correct. instanceof uses the prototype chain. Following the line Student.prototype = Object.create(Person.prototype), we've replaced the original Student.prototype and Student.prototype.constructor would point to Person. I am currently fixing the response to remove this confusion.
  • bthehuman
    bthehuman over 8 years
    @FelixKling I also admit I'm confused by my own wording there. I mean to say in ES6, the constructor is actually a method given to us (albeit syntactic sugar) with keywords like super for convenience - as opposed to being a 'plain' function that is invoked with new.
  • Dmitri Zaitsev
    Dmitri Zaitsev about 8 years
    Your example is using this.constructor internally, so it is not surprising it has to be set. Do you have any example without it?
  • abhisekp
    abhisekp almost 8 years
    @RoumelisGeorge Object.create would create an object with the student1 as its [[Prototype]] and not Student.prototype as its [[Prototype]].
  • RaelB
    RaelB almost 8 years
    One point to add to this answer is mentioned in T. J. Crowder's answer. When you define the Student function, it's prototype.constructor is Student. However, when Student.prototype = Object.create(Person.prototype); is called, a negative side effect is that the Student.prototype.constructor property is set to Person. Hence this can be fixed by setting the prototype.constructor back to Student.
  • Drenai
    Drenai over 7 years
    The linked article referenced in the question alreay uses Object.create(). This answer and the Edit ot the answer are not really relevant, and it's confusing to say the least:-)
  • James D
    James D over 7 years
    The broader point is that there are gotchas that will snag people new to Javascript prototypes. If we're discussing in 2016, then you should really use ES6 classes, Babel, and/or Typescript. But if you really want to manually construct classes this way, it helps to understand how prototype chains really work to leverage their power. You can use any object as a prototype, and maybe you don't want to new a separate object. Furthermore, back before HTML 5 was fully widespread, Object.create was not always available, so it was easier to set up a class incorrectly.
  • Ry-
    Ry- about 6 years
    What is any of this supposed to do? foo.constructor()??
  • Rafał Spryszyński
    Rafał Spryszyński almost 6 years
    What a great answer! It is that simple. No need to work with prototypes directly if you know what you're doing.
  • Ini
    Ini over 4 years
    I would not call this reset. Your new constructor property just gets found earlier in the prototype chain.
  • Wayne
    Wayne over 3 years
    @CEGRD I would also like to know what the heck I meant by that "just as bad" comment 🧐