web-dev-qa-db-fra.com

Pourquoi est-il nécessaire de définir le constructeur du prototype?

Dans la section sur l'héritage dans l'article MDN Introduction à JavaScript , j'ai remarqué qu'ils définissaient le prototype.constructor:

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

Est-ce que cela sert à quelque chose d'important? Est-ce que ça va de l'omettre?

269
trinth

Ce n'est pas toujours nécessaire, mais cela a ses utilisations. Supposons que nous voulions créer une méthode de copie dans la classe Person de base. Comme ça:

// 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);

Que se passe-t-il maintenant lorsque nous créons une nouvelle variable Student et la copions? 

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

La copie n'est pas une instance de Student. En effet, sans contrôle explicite, nous n’aurions aucun moyen de renvoyer une copie Student à partir de la classe "base". Nous ne pouvons que renvoyer une Person. Cependant, si nous avions réinitialisé le constructeur:

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

... alors tout fonctionne comme prévu:

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

Est-ce que cela sert à quelque chose d'important?

Oui et non.

Dans ES5 et les versions antérieures, JavaScript lui-même n’utilisait constructor pour rien. Il a défini que l'objet par défaut de la propriété prototype d'une fonction l'aurait et qu'il renverrait à la fonction, et que c'était lui. Rien d'autre dans la spécification n'y fait référence.

Cela a changé dans ES2015 (ES6), qui a commencé à l'utiliser en relation avec les hiérarchies d'héritage. Par exemple, Promise#then utilise la propriété constructor de la promesse sur laquelle vous l'appelez (via SpeciesConstructor ) lors de la création de la nouvelle promesse de retour. Il est également impliqué dans les tableaux de sous-typage (via ArraySpeciesCreate ).

En dehors du langage lui-même, les gens l'utilisaient parfois pour créer des fonctions génériques de "clonage" ou simplement pour faire référence à ce qu'ils pensaient être la fonction constructeur de l'objet. Mon expérience est que l’utiliser est rare, mais parfois les gens l’utilisent.

Est-ce que ça va de l'omettre?

C'est là par défaut, vous n'avez besoin de le remettre que lorsque vous replacez l'objet sur la propriété prototype d'une fonction:

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

Si vous ne faites pas ceci:

Student.prototype.constructor = Student;

... then Student.prototype.constructor hérite de Person.prototype qui (probablement) a constructor = Person. Donc, c'est trompeur. Et bien sûr, si vous sous-classez quelque chose qui l'utilise (comme Promise ou Array) et n'utilisez pas class¹ (qui gère cela pour vous), vous voudrez vous assurer de le définir correctement. Donc en gros: c'est une bonne idée.

Ce n'est pas grave si rien dans votre code (ou le code de la bibliothèque que vous utilisez) ne l'utilise. Je me suis toujours assuré qu'il était correctement branché.

Bien sûr, avec le mot clé class de ES2015 (ou ES6), nous l'utilisions la plupart du temps, nous n'y sommes plus obligés, car il est géré pour nous lorsque nous le faisons.

class Student extends Person {
}

¹ "... si vous sous-classez quelque chose qui l'utilise (comme Promise ou Array) et n'utilisez pas class..." - C'est possible de le faire, mais c'est une vraie douleur (et un peu bête). Vous devez utiliser Reflect.construct .

66
T.J. Crowder

TLDR; Pas super nécessaire, mais aidera probablement à long terme, et il est plus précis de le faire.

NOTE: Bien édité car ma réponse précédente était écrite avec confusion et avait quelques erreurs que je manquais dans ma précipitation à répondre. Merci à ceux qui ont signalé certaines erreurs flagrantes.

En gros, il s'agit de câbler correctement les sous-classes en Javascript. Lorsque nous sous-classons, nous devons faire certaines choses géniales pour nous assurer que la délégation de prototypes fonctionne correctement, y compris en écrasant un objet prototype. Le remplacement d’un objet prototype inclut le constructor, nous devons donc corriger la référence.

Passons rapidement en revue le fonctionnement des «classes» dans ES5.

Disons que vous avez une fonction constructeur et son 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',
}

Lorsque vous appelez le constructeur pour instancier, dites Adam:

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

Le mot clé new appelé avec 'Person' lancera le constructeur Person avec quelques lignes de code supplémentaires:

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
 * }
 */

Si nous console.log(adam.species), la recherche échouera à l'instance adam et la chaîne du prototype à son .prototype, qui est Person.prototype - et Person.prototypea une propriété .species, afin que la recherche aboutisse à Person.prototype. Il va ensuite enregistrer 'human'.

Ici, le Person.prototype.constructor pointera correctement sur Person.

Alors maintenant, la partie intéressante, le soi-disant «sous-classement». Si nous voulons créer une classe Student, c'est-à-dire une sous-classe de la classe Person avec quelques modifications supplémentaires, nous devrons nous assurer que le Student.prototype.constructor pointe vers Student pour plus de précision.

Il ne fait pas cela par lui-même. Lorsque vous sous-classe, le code ressemble à ceci:

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: {}

Appeler new Student() ici renverrait un objet avec toutes les propriétés souhaitées. Ici, si nous vérifions eve instanceof Person, il retournera false. Si nous essayons d'accéder à eve.species, il renverrait undefined.

En d'autres termes, nous devons câbler la délégation afin que eve instanceof Person renvoie true et que les instances de Student délèguent correctement à Student.prototype, puis à Person.prototype.

MAIS puisque nous l'appelons avec le mot clé new, rappelez-vous ce que cette invocation ajoute? Il s’appellerait Object.create(Student.prototype), et c’est ainsi que nous avons configuré cette relation de délégation entre Student et Student.prototype. Notez que pour l'instant, Student.prototype est vide. Donc, rechercher .species une instance de Student échouerait car elle délègue à onlyStudent.prototype, et la propriété .species n'existe pas sur Student.prototype.

Lorsque nous assignons Student.prototype à Object.create(Person.prototype), Student.prototype lui-même, puis délègue à Person.prototype, et rechercher eve.species renverra human comme prévu. Vraisemblablement, nous voudrions qu'il hérite de Student.prototype AND Person.prototype. Nous devons donc régler tout cela.

/* 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);

La délégation fonctionne maintenant, mais nous remplaçons Student.prototype par un Person.prototype. Donc, si nous appelons Student.prototype.constructor, cela indiquerait Person au lieu de Student. Ceci est la raison pour laquelle nous devons le réparer. 

// 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

Dans ES5, notre propriété constructor est une référence qui fait référence à une fonction que nous avons écrite dans l’intention d’être un «constructeur». Mis à part ce que nous donne le mot clé new, le constructeur est sinon une fonction 'plain'.

Dans ES6, constructor est désormais intégré à la façon dont nous écrivons les classes. En tant que tel, il est fourni comme méthode lorsque nous déclarons une classe. C'est simplement du sucre syntaxique, mais cela nous offre certaines commodités comme l'accès à un super lorsque nous étendons une classe existante. Donc nous écririons le code ci-dessus comme ceci:

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;
   }
}
11
bthehuman

Je ne suis pas d'accord. Il n'est pas nécessaire de définir le prototype. Prenez exactement le même code mais supprimez la ligne prototype.constructor. Est-ce que quelque chose change? Non. Maintenant, apportez les modifications suivantes:

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

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

et à la fin du code de test ...

alert(student1.favoriteColor);

La couleur sera bleue.

D'après mon expérience, un changement de prototype.constructor ne fait pas grand chose à moins que vous ne fassiez des choses très spécifiques, très compliquées qui ne sont probablement pas de bonnes pratiques de toute façon :)

Edit: Après avoir fouillé sur le Web pendant un moment et fait quelques expériences, il semble que les gens configurent le constructeur de manière à ce qu'il "ressemble" à ce qui est construit avec "nouveau". Je suppose que je dirais que le problème avec ceci est que javascript est un langage prototype - il n'y a pas d'héritage. Mais la plupart des programmeurs viennent d'un contexte de programmation qui considère l'héritage comme «la voie à suivre». Nous proposons donc toutes sortes de choses pour essayer de faire de ce langage prototypique un langage "classique", comme l'extension de "classes". En réalité, dans l'exemple qu'ils ont donné, un nouvel élève est une personne - ce n'est pas «s'étendre» d'un autre élève… l'élève est tout au sujet de la personne et quelle que soit la personne est l'élève également. Prolongez l'étudiant, et ce que vous avez étendu est un élève au cœur, mais il est personnalisé pour répondre à vos besoins. 

Crockford est un peu fou et trop zélé, mais faites une lecture sérieuse de certaines choses qu'il a écrites… cela vous fera regarder ces choses très différemment.

10
Stephen

Cela a l'énorme piège que si vous avez écrit 

Student.prototype.constructor = Student;

mais alors s'il y avait un Enseignant dont le prototype était aussi Personne et vous avez écrit

Teacher.prototype.constructor = Teacher;

alors le constructeur étudiant est maintenant enseignant!

Edit: Vous pouvez éviter cela en vous assurant que vous avez défini les prototypes Student et Teacher à l'aide de nouvelles instances de la classe Person créées à l'aide de Object.create, comme dans l'exemple Mozilla.

Student.prototype = Object.create(Person.prototype);
Teacher.prototype = Object.create(Person.prototype);
9
James D

Jusqu'à présent, la confusion est toujours là.

Suivant l'exemple original, vous avez un objet existant student1 comme:

var student1 = new Student("Janet", "Applied Physics");

Supposons que vous ne voulez pas savoir comment student1 est créé, vous voulez simplement un autre objet similaire, vous pouvez utiliser la propriété constructeur de student1 comme:

var student2 = new student1.constructor("Mark", "Object-Oriented JavaScript");

Dans ce cas, les propriétés de Student ne pourront pas être obtenues si la propriété du constructeur n'est pas définie. Cela créera plutôt un objet Person.

5
Mahavir

Vous avez un exemple de code de Nice expliquant pourquoi il est vraiment nécessaire de définir le constructeur du prototype.

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 ???? 
*/
2
user3877965

Pas besoin de fonction 'classes' sucrée ou en utilisant 'Nouveau' ces jours-ci. Utilisez des littéraux d'objet.

Le prototype d'objet est déjà une "classe". Lorsque vous définissez un littéral d'objet, il s'agit déjà d'une instance de l'objet prototype. Ceux-ci peuvent également servir de prototype d'un autre objet, etc.

const Person = {
  name: '[Person.name]',
  greeting: function() {
    console.log( `My name is ${ this.name || '[Name not assigned]' }` );
  }
};
// Person.greeting = function() {...} // or define outside the obj if you must

// Object.create version
const john = Object.create( Person );
john.name = 'John';
console.log( john.name ); // John
john.greeting(); // My name is John 
// Define new greeting method
john.greeting = function() {
    console.log( `Hi, my name is ${ this.name }` )
};
john.greeting(); // Hi, my name is John

// Object.assign version
const jane = Object.assign( Person, { name: 'Jane' } );
console.log( jane.name ); // Jane
// Original greeting
jane.greeting(); // My name is Jane 

// Original Person obj is unaffected
console.log( Person.name ); // [Person.name]
console.log( Person.greeting() ); // My name is [Person.name]

Cela vaut la peine d'être lu :

Les langages orientés objet basés sur des classes, tels que Java et C++, sont fondée sur le concept de deux entités distinctes: les classes et les instances.

...

Un langage basé sur un prototype, tel que JavaScript, ne crée pas cela distinction: il a simplement des objets. Un langage basé sur un prototype a le notion d'objet prototype, objet utilisé comme modèle à partir de pour obtenir les propriétés initiales d’un nouvel objet. Tout objet peut spécifier ses propres propriétés, soit lors de sa création, soit au moment de l'exécution . De plus, tout objet peut être associé en tant que prototype à un autre objet, permettant au second objet de partager le .__ du premier objet. Propriétés

1
ucsarge

C'est nécessaire lorsque vous avez besoin d'une alternative à toString sans monkeypage

//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"]);

1
Paul Sweatte

EDIT, je me suis vraiment trompé. Commenter la sortie ne change pas du tout son comportement. (Je l'ai testé)


Oui il faut. Quand tu fais 

Student.prototype = new Person();  

Student.prototype.constructor devient Person. Par conséquent, appeler Student() renverrait un objet créé par Person. Si vous faites alors

Student.prototype.constructor = Student; 

Student.prototype.constructor est réinitialisé à Student. Maintenant, lorsque vous appelez Student(), il exécute Student, qui appelle le constructeur parent Parent(), renvoie l'objet correctement hérité. Si vous n'avez pas réinitialisé Student.prototype.constructor avant de l'appeler, vous obtiendrez un objet pour lequel aucune des propriétés ne sera définie dans Student().

0
invisible bob

Étant donné la fonction constructeur simple:

function Person(){
    this.name = 'test';
}


console.log(Person.prototype.constructor) // function Person(){...}

Person.prototype = { //constructor in this case is Object
    sayName: function(){
        return this.name;
    }
}

var person = new Person();
console.log(person instanceof Person); //true
console.log(person.sayName()); //test
console.log(Person.prototype.constructor) // function Object(){...}

Par défaut (à partir de la spécification https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor ), tous les prototypes reçoivent automatiquement une propriété appelée constructeur qui pointe en arrière. à la fonction sur laquelle il s'agit d'une propriété. Selon le constructeur, d’autres propriétés et méthodes peuvent être ajoutées au prototype, ce qui n’est pas une pratique très courante mais qui est néanmoins autorisée pour les extensions. 

Donc, répondez simplement: nous devons nous assurer que la valeur de prototype.constructor est correctement définie, comme le suppose la spécification.

Devons-nous toujours définir correctement cette valeur? Cela facilite le débogage et rend la structure interne cohérente par rapport aux spécifications. Nous devrions certainement lorsque notre API est utilisée par les tiers, mais pas vraiment lorsque le code est finalement exécuté dans le runtime.

0
kospiotr

Voici un exemple de MDN que j'ai trouvé très utile pour comprendre ses utilisations.

En JavaScript, nous avons async functions qui retourne AsyncFunction object. AsyncFunction n'est pas un objet global, mais vous pouvez le récupérer en utilisant la propriété constructor et l'utiliser.

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

// AsyncFunction constructor
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor

var a = new AsyncFunction('a', 
                          'b', 
                          'return await resolveAfter2Seconds(a) + await resolveAfter2Seconds(b);');

a(10, 20).then(v => {
  console.log(v); // prints 30 after 4 seconds
});
0
Hitesh Kumar