Perché è necessario impostare il costruttore prototipo?

Nella sezione sull’ereditarietà nell’articolo MDN Introduzione al Javascript orientato agli oggetti , ho notato che imposta il prototype.constructor:

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

Questo ha qualche scopo importante? Va bene ometterlo?

Non è sempre necessario, ma ha i suoi usi. Supponiamo di voler creare un metodo di copia sulla class di base di Person . Come questo:

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

Ora cosa succede quando creiamo un nuovo Student e lo copia?

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

La copia non è un’istanza di Student . Questo perché (senza controlli espliciti), non avremmo modo di restituire una copia di Student dalla class “base”. Possiamo solo restituire una Person . Tuttavia, se avessimo ripristinato il costruttore:

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

… quindi tutto funziona come previsto:

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

Questo ha qualche scopo importante?

Sì e no.

In ES5 e versioni precedenti, JavaScript non ha utilizzato il constructor per nulla. Ha definito che l’object predefinito sulla proprietà del prototype una funzione avrebbe esso e che farebbe riferimento alla funzione, e così è stato . Nient’altro nella specifica si riferiva a questo.

Ciò è cambiato in ES2015 (ES6), che ha iniziato a utilizzarlo in relazione alle gerarchie di ereditarietà. Ad esempio, Promise#then utilizza Promise#then la proprietà del constructor della promise su cui lo si chiama (tramite SpeciesConstructor ) quando si costruisce la nuova promise di restituzione. È anche coinvolto negli array di sottotitoli (tramite ArraySpeciesCreate ).

Al di fuori del linguaggio stesso, a volte le persone lo userebbero quando cercavano di build funzioni generiche di “clone” o solo in generale quando volevano riferirsi a ciò che credevano fosse la funzione di costruzione dell’object. La mia esperienza è che usarlo è raro, ma a volte le persone lo usano.

Va bene ometterlo?

È lì per impostazione predefinita, è necessario rimetterlo solo quando si sostituisce l’object sulla proprietà prototype una funzione:

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

Se non lo fai:

 Student.prototype.constructor = Student; 

… quindi Student.prototype.constructor eredita da Person.prototype che (presumibilmente) ha constructor = Person . Quindi è fuorviante. E, naturalmente, se stai sottoclassi qualcosa che lo usa (come Promise o Array ) e non utilizzi la class ¹ (che gestisce questo per te), ti consigliamo di assicurarti di averlo impostato correttamente. Quindi in pratica: è una buona idea.

Va bene se nulla nel tuo codice (o codice della libreria che usi) lo usa. Ho sempre assicurato che fosse collegato correttamente.

Ovviamente, con la parola chiave di class ES2015 (nota anche come ES6), la maggior parte delle volte l’avremmo usata, non ne abbiamo più, perché è gestita per noi quando lo facciamo

 class Student extends Person { } 

¹ “… se stai sottoclassi qualcosa che lo usa (come Promise o Array ) e non usi la class …” – È ansible farlo, ma è un vero dolore (e un po ‘sciocco). Devi usare Reflect.construct .

Non sarei d’accordo. Non è necessario impostare il prototipo. Prendi lo stesso identico codice ma rimuovi la linea prototype.constructor. Qualcosa cambia? No. Ora apporta le seguenti modifiche:

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

e alla fine del codice di test …

 alert(student1.favoriteColor); 

Il colore sarà blu.

Una modifica al prototype.constructor, nella mia esperienza, non fa molto a meno che tu non stia facendo cose molto specifiche e molto complicate che probabilmente non sono comunque buone pratiche 🙂

Modifica: dopo aver esplorato il web per un po ‘e aver fatto qualche esperimento, sembra che le persone impostino il costruttore in modo che “assomigli” alla cosa che viene costruita con “nuovo”. Suppongo che sosterrei che il problema con questo è che javascript è un linguaggio prototipo – non esiste una cosa come l’ereditarietà. Ma la maggior parte dei programmatori proviene da uno sfondo di programmazione che spinge l’ereditarietà come “la via”. Quindi arriviamo con ogni sorta di cose per cercare di rendere questo linguaggio prototipo un linguaggio ‘classico’ … come estendere ‘classi’. In realtà, nell’esempio che hanno dato, un nuovo studente è una persona – non si sta ‘estendendo’ da un altro studente .. lo studente è tutto sulla persona, e qualunque sia la persona che è anche lo studente. Estendi lo studente e qualsiasi cosa tu abbia esteso è uno studente nel cuore, ma è personalizzato in base alle tue esigenze.

Crockford è un po ‘pazzo e troppo zelante, ma fa alcune letture seri su alcune delle cose che ha scritto … ti farà guardare a queste cose in modo molto diverso.

TLDR; Non super necessario, ma probabilmente sarà di aiuto a lungo termine, ed è più accurato farlo.

NOTA: Molto modificato poiché la mia risposta precedente era scritta in modo confuso e aveva alcuni errori che mi mancavano nella fretta di rispondere. Grazie a coloro che hanno sottolineato alcuni errori eclatanti.

Fondamentalmente, è per colbind sottoclassi in modo corretto in Javascript. Quando eseguiamo la sottoclass, dobbiamo fare alcune cose funky per assicurarci che la delega prototipale funzioni correttamente, inclusa la sovrascrittura di un object prototype . La sovrascrittura di un object prototype include il constructor , quindi è necessario correggere il riferimento.

Vediamo rapidamente come funzionano le “classi” in ES5.

Supponiamo che tu abbia una funzione di costruzione e il suo prototipo:

 //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', } 

Quando chiami il costruttore per istanziare, dì Adam :

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

Fondamentalmente, la new parola chiave invocata con “Persona” eseguirà il costruttore Person con alcune righe di codice aggiuntive:

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

Se usiamo console.log(adam.species) , la ricerca fallirà nell’istanza di adam e cercheremo la catena prototipale nel suo .prototype , che è Person.prototype – e Person.prototype ha una proprietà .species , quindi la ricerca riuscirà su Person.prototype . Quindi verrà registrato 'human' .

Qui, Person.prototype.constructor indicherà correttamente Person .

Quindi ora la parte interessante, la cosiddetta “sottoclass”. Se vogliamo creare una class Student , che è una sottoclass della class Person con alcune modifiche aggiuntive, dovremo assicurarci che lo Student.prototype.constructor punti su Studente per la precisione.

Non lo fa da solo. Quando si sottoclass, il codice è simile al seguente:

 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() qui restituirebbe un object con tutte le proprietà che vogliamo. Qui, se controlliamo l’ eve instanceof Person , restituirebbe false . Se proviamo ad accedere a eve.species , restituirebbe undefined .

In altre parole, dobbiamo cablare la delega in modo che Student.prototype instance of eve instanceof Person ritorni vera e che le istanze di Student delegano correttamente a Student.prototype e quindi a Person.prototype .

MA dato che lo chiamiamo con la new parola chiave, ricorda cosa aggiunge quella chiamata? Chiamerebbe Object.create(Student.prototype) , che è il modo in cui impostiamo la relazione di delega tra Student e Student.prototype . Nota che al momento, Student.prototype è vuoto. Quindi, osservando .species un’istanza di Student fallirebbe in quanto delegava solo a Student.prototype , e la proprietà .species non esiste su Student.prototype .

Quando assegniamo Student.prototype a Object.create(Person.prototype) , Student.prototype stesso Student.prototype delega a Person.prototype , e guardando eve.species tornerà human come ci aspettiamo. Presumibilmente vorremmo che ereditasse da Student.prototype AND Person.prototype. Quindi dobbiamo sistemare tutto questo.

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

Ora la delega funziona, ma sovrascriviamo Student.prototype con un Person.prototype . Quindi, se chiamiamo Student.prototype.constructor , farebbe riferimento a Person anziché a Student . Questo è il motivo per cui dobbiamo risolverlo.

 // 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, la nostra proprietà del constructor è un riferimento che fa riferimento a una funzione che abbiamo scritto con l’intento di essere un “costruttore”. A prescindere da ciò che ci offre la new parola chiave, il costruttore è altrimenti una funzione “semplice”.

In ES6, il constructor è ora integrato nel modo in cui scriviamo le classi – come in, è fornito come metodo quando dichiariamo una class. Questo è semplicemente zucchero sintattico, ma ci accorda alcune comodità come l’accesso a un super quando estendiamo una class esistente. Quindi dovremmo scrivere il codice sopra in questo modo:

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

Questo ha l’enorme trappola che se hai scritto

 Student.prototype.constructor = Student; 

ma poi se c’era un Insegnante il cui prototipo era anche Persona e tu hai scritto

 Teacher.prototype.constructor = Teacher; 

allora il costruttore Studente ora è Insegnante!

Modifica: puoi evitarlo assicurandoti di aver impostato i prototipi Studente e Insegnante usando nuove istanze della class Person creata usando Object.create, come nell’esempio di Mozilla.

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

Finora la confusione è ancora lì.

Seguendo l’esempio originale, come hai un object student1 esistente come:

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

Supponiamo che tu non voglia sapere come viene creato lo student1 , vuoi solo un altro object come questo, puoi usare la proprietà del costruttore di student1 come:

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

Qui non riuscirà a ottenere le proprietà da Student se la proprietà del costruttore non è impostata. Piuttosto creerà un object Person .

Ho un buon esempio di codice del motivo per cui è davvero necessario impostare il costruttore del prototipo.

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

Non c’è bisogno di “classi” di funzioni zuccherate o di “Nuovi” in questi giorni. Usa letterali object.

Il prototipo Object è già una “class”. Quando definisci un object letterale, è già un’istanza dell’object prototipo. Questi possono anche fungere da prototipo di un altro object, ecc.

 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] 

Vale la pena leggere :

I linguaggi orientati agli oggetti basati sulla class, come Java e C ++, sono basati sul concetto di due quadro distinte: classi e istanze.

Un linguaggio basato su prototipi, come JavaScript, non fa questa distinzione: ha semplicemente degli oggetti. Un linguaggio basato sul prototipo ha la nozione di un object prototipo, un object usato come modello da cui ottenere le proprietà iniziali per un nuovo object. Qualsiasi object può specificare le sue proprietà, sia quando lo crei o in fase di esecuzione. Inoltre, qualsiasi object può essere associato come prototipo per un altro object, consentendo al secondo object di condividere le proprietà del primo object

È necessario quando hai bisogno di un’alternativa a toString senza 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"]); 

EDIT, in realtà ero sbagliato. Commentare l’allineamento non cambia affatto il suo comportamento. (L’ho provato)


Sì, è necessario. Quando lo fai

 Student.prototype = new Person(); 

Student.prototype.constructor diventa Person . Pertanto, chiamando Student() restituirebbe un object creato da Person . Se lo fai allora

 Student.prototype.constructor = Student; 

Student.prototype.constructor viene reimpostato su Student . Ora quando chiami Student() esegue Student , che chiama il genitore costruttore Parent() , restituisce l’object ereditato correttamente. Se non hai resettato Student.prototype.constructor prima di chiamarlo, Student.prototype.constructor un object che non avrebbe nessuna delle proprietà impostate in Student() .

Data la semplice funzione di costruzione:

 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(){...} 

Per impostazione predefinita (dalla specifica https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor ), tutti i prototipi ottengono automaticamente una proprietà chiamata costruttore che punta alla funzione su che è una proprietà. A seconda del costruttore, altre proprietà e metodi potrebbero essere aggiunti al prototipo, che non è una pratica molto comune, ma è comunque consentita per le estensioni.

Quindi, semplicemente rispondendo: dobbiamo assicurarci che il valore in prototype.constructor sia impostato correttamente come previsto dalle specifiche.

Dobbiamo sempre impostare correttamente questo valore? Aiuta con il debug e rende la struttura interna coerente con le specifiche. Dovremmo sicuramente quando la nostra API viene utilizzata dalla terza parte, ma non proprio quando il codice viene finalmente eseguito nel runtime.

Non è necessario. È solo una delle tante cose tradizionali, i campioni OOP fanno per provare a trasformare l’eredità prototipica di JavaScript in ereditarietà classica. L’unica cosa che segue

 Student.prototype.constructor = Student; 

fa, è che ora hai un riferimento all’attuale “costruttore”.

Nella risposta di Wayne, che è stata contrassegnata come corretta, potresti esattamente la stessa cosa che fa il seguente codice

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

con il codice qui sotto (basta sostituire this.constructor con Person)

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

Grazie a Dio, con i puristi dell’ereditarietà classica dell’ES6 possiamo utilizzare gli operatori nativi del linguaggio come class, extends e super e non dobbiamo vedere come correzioni prototype.constructor e riferimenti parentali.