Why is it necessary to set the prototype construct

2018-12-31 05:50发布

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?

12条回答
姐姐魅力值爆表
2楼-- · 2018-12-31 06:16

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;
   }
}
查看更多
查无此人
3楼-- · 2018-12-31 06:19

EDIT, I was actually wrong. Commenting the line out doesn't change it's behavior at all. (I tested it)


Yes, it is necessary. When you do

Student.prototype = new Person();  

Student.prototype.constructor becomes Person. Therefore, calling Student() would return an object created by Person. If you then do

Student.prototype.constructor = Student; 

Student.prototype.constructor is reset back to Student. Now when you call Student() it executes Student, which calls the parent constructor Parent(), it returns the correctly inherited object. If you didn't reset Student.prototype.constructor before calling it you would get an object that would not have any of the properties set in Student().

查看更多
浮光初槿花落
4楼-- · 2018-12-31 06:20

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);
查看更多
裙下三千臣
5楼-- · 2018-12-31 06:20

Got a nice code example of why it is really necessary to set the prototype constructor..

function CarFactory(name){ 
   this.name=name;  
} 
CarFactory.prototype.CreateNewCar = function(){ 
    return new this.constructor("New Car "+ this.name); 
} 
CarFactory.prototype.toString=function(){ 
    return 'Car Factory ' + this.name;
} 

AudiFactory.prototype = new CarFactory();      // Here's where the inheritance occurs 
AudiFactory.prototype.constructor=AudiFactory;       // Otherwise instances of Audi would have a constructor of Car 

function AudiFactory(name){ 
    this.name=name;
} 

AudiFactory.prototype.toString=function(){ 
    return 'Audi Factory ' + this.name;
} 

var myAudiFactory = new AudiFactory('');
  alert('Hay your new ' + myAudiFactory + ' is ready.. Start Producing new audi cars !!! ');            

var newCar =  myAudiFactory.CreateNewCar(); // calls a method inherited from CarFactory 
alert(newCar); 

/*
Without resetting prototype constructor back to instance, new cars will not come from New Audi factory, Instead it will come from car factory ( base class )..   Dont we want our new car from Audi factory ???? 
*/
查看更多
孤独总比滥情好
6楼-- · 2018-12-31 06:23

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.

查看更多
有味是清欢
7楼-- · 2018-12-31 06:24

It is necessary when you need an alternative to toString without monkeypatching:

//Local
foo = [];
foo.toUpperCase = String(foo).toUpperCase;
foo.push("a");
foo.toUpperCase();

//Global
foo = [];
window.toUpperCase = function (obj) {return String(obj).toUpperCase();}
foo.push("a");
toUpperCase(foo);

//Prototype
foo = [];
Array.prototype.toUpperCase = String.prototype.toUpperCase;
foo.push("a");
foo.toUpperCase();

//toString alternative via Prototype constructor
foo = [];
Array.prototype.constructor = String.prototype.toUpperCase;
foo.push("a,b");
foo.constructor();

//toString override
var foo = [];
foo.push("a");
var bar = String(foo);
foo.toString = function() { return bar.toUpperCase(); }
foo.toString();

//Object prototype as a function
Math.prototype = function(char){return Math.prototype[char]};
Math.prototype.constructor = function() 
  {
  var i = 0, unicode = {}, zero_padding = "0000", max = 9999;
  
  while (i < max) 
    {
    Math.prototype[String.fromCharCode(parseInt(i, 16))] = ("u" + zero_padding + i).substr(-4);

    i = i + 1;
    }    
  }

Math.prototype.constructor();
console.log(Math.prototype("a") );
console.log(Math.prototype["a"] );
console.log(Math.prototype("a") === Math.prototype["a"]);

查看更多
登录 后发表回答