javascript - oriented - node js class inheritance example




Por que é necessário definir o construtor de protótipo? (9)

Na seção sobre herança no artigo do MDN Introdução ao JavaScript orientado a objetos , notei que eles configuram o prototype.constructor:

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

Isso serve a algum propósito importante? Está tudo bem omitir isso?


Isso serve a algum propósito importante?

Sim e não.

No ES5 e anterior, o próprio JavaScript não usava constructor para nada. Definiu que o objeto padrão na propriedade de prototype uma função o teria e que se referiria à função, e foi isso . Nada mais na especificação se referia a ela.

Isso mudou no ES2015 (ES6), que começou a usá-lo em relação às hierarquias de herança. Por exemplo, Promise#then usa a propriedade constructor da promessa na qual você a chama (via SpeciesConstructor ) ao criar a nova promessa a ser retornada. Ele também está envolvido na subtipação de matrizes (via ArraySpeciesCreate ).

Fora da própria linguagem, às vezes as pessoas a usavam quando tentavam construir funções genéricas de "clone" ou, em geral, quando queriam se referir ao que acreditavam ser a função construtora do objeto. Minha experiência é que usá-lo é raro, mas às vezes as pessoas o usam.

Está tudo bem omitir isso?

Está lá por padrão, você só precisa colocá-lo de volta quando você substituir o objeto na propriedade prototype uma função:

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

Se você não fizer isso:

Student.prototype.constructor = Student;

... então Student.prototype.constructor herda de Person.prototype que (presumivelmente) tem constructor = Person . Então é enganoso. E, claro, se você está subclassificando algo que a usa (como Promise ou Array ) e não está usando a class ¹ (que lida com isso para você), você vai querer certificar-se de configurá-lo corretamente. Então, basicamente: é uma boa ideia.

Tudo bem se nada em seu código (ou código de biblioteca que você usa) usa. Eu sempre assegurei que estava corretamente conectado.

Claro que, com a palavra-chave class ES2015 (aka ES6), na maioria das vezes nós o teríamos usado, não precisamos mais, porque ele é tratado por nós quando fazemos

class Student extends Person {
}

¹ "... se você está subclassificando algo que o usa (como Promise ou Array ) e não usando class ..." - É possível fazer isso, mas é uma dor real (e um pouco boba). Você tem que usar Reflect.construct .


EDIT, eu estava realmente errado. Comentar a linha não muda o seu comportamento. (Eu testei)

Sim, é necessário. Quando você faz

Student.prototype = new Person();  

Student.prototype.constructor se torna Person . Portanto, chamar Student() retornaria um objeto criado por Person . Se você fizer

Student.prototype.constructor = Student; 

Student.prototype.constructor é redefinido de volta para Student . Agora quando você chama Student() ele executa Student , que chama o construtor pai Parent() , ele retorna o objeto herdado corretamente. Se você não redefinir o Student.prototype.constructor antes de chamá-lo, obteria um objeto que não teria nenhuma das propriedades definidas em Student() .


Aqui está um exemplo do MDN que eu achei muito útil para entender seus usos.

Em JavaScript, temos async functions que retornam o objeto AsyncFunction . AsyncFunction não é um objeto global, mas é possível recuperá-lo usando a propriedade constructor e utilizá-lo.

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

Até agora a confusão ainda está lá.

Seguindo o exemplo original, como você tem um objeto existente student1 como:

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

Suponha que você não queira saber como o student1 é criado, você só quer outro objeto como este, você pode usar a propriedade constructor do student1 como:

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

Aqui, ele não conseguirá obter as propriedades de Student se a propriedade do construtor não estiver definida. Em vez disso, criará um objeto Person .


Eu discordaria. Não é necessário definir o protótipo. Tome exatamente o mesmo código, mas remova a linha prototype.constructor. Alguma coisa muda? Não. Agora, faça as seguintes alterações:

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

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

e no final do código de teste ...

alert(student1.favoriteColor);

A cor será azul.

Uma mudança no prototype.constructor, na minha experiência, não faz muito a menos que você esteja fazendo coisas muito específicas, muito complicadas que provavelmente não são boas práticas de qualquer maneira :)

Edit: Depois de bisbilhotar um pouco a web e fazer algumas experiências, parece que as pessoas definem o construtor para que ele 'pareça' com o que está sendo construído com 'new'. Eu acho que eu diria que o problema com isso é que o javascript é uma linguagem protótipo - não existe heranças. Mas a maioria dos programadores vem de um histórico de programação que impõe a hereditariedade como "o caminho". Por isso, criamos todo o tipo de coisas para tentar transformar esta linguagem prototípica numa linguagem "clássica" ... como, por exemplo, a extensão de "classes". Realmente, no exemplo que deram, um novo aluno é uma pessoa - não está "se estendendo" de outro aluno ... o aluno é todo sobre a pessoa, e qualquer que seja a pessoa que é o aluno também é. Estenda o aluno, e o que quer que você tenha estendido é um estudante no coração, mas é personalizado para atender às suas necessidades.

Crockford é um pouco louco e com excesso de zelo, mas faz algumas leituras sérias sobre algumas das coisas que ele escreveu ... isso vai fazer você ver essas coisas de maneira muito diferente.


Isso tem a enorme armadilha que se você escreveu

Student.prototype.constructor = Student;

mas se houvesse um professor cujo protótipo também fosse Person e você escrevesse

Teacher.prototype.constructor = Teacher;

então o construtor Student é agora Teacher!

Edit: Você pode evitar isso garantindo que você tenha configurado os protótipos de Student e Teacher usando novas instâncias da classe Person criada usando Object.create, como no exemplo do Mozilla.

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

Não há necessidade de 'classes' com função açucarada ou usar 'New' nos dias de hoje. Use literais de objeto.

O protótipo do objeto já é uma 'classe'. Quando você define um objeto literal, ele já é uma instância do protótipo Object. Estes também podem atuar como o protótipo de outro objeto, 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]

Vale a pena ler isto :

Linguagens orientadas a objetos baseadas em classes, como Java e C ++, são baseadas no conceito de duas entidades distintas: classes e instâncias.

...

Uma linguagem baseada em protótipos, como JavaScript, não faz essa distinção: ela simplesmente possui objetos. Uma linguagem baseada em protótipo tem a noção de um objeto prototípico, um objeto usado como modelo a partir do qual obter as propriedades iniciais de um novo objeto. Qualquer objeto pode especificar suas próprias propriedades, quando você o cria ou em tempo de execução. Além disso, qualquer objeto pode ser associado como o protótipo de outro objeto, permitindo que o segundo objeto compartilhe as propriedades do primeiro objeto.


Nem sempre é necessário, mas tem seus usos. Suponha que desejamos fazer um método de cópia na classe Person base. Como isso:

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

Agora, o que acontece quando criamos um novo Student e o copiamos?

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

A cópia não é uma instância do Student . Isso ocorre porque (sem verificações explícitas) não teríamos como retornar uma cópia do Student da classe "base". Nós só podemos retornar uma Person . No entanto, se tivéssemos redefinido o construtor:

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

... então tudo funciona como esperado:

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

TLDR; Não é super necessário, mas provavelmente ajudará a longo prazo, e é mais preciso fazê-lo.

NOTA: Muito editado como a minha resposta anterior foi confusamente escrita e tive alguns erros que eu perdi na minha pressa para responder. Obrigado àqueles que apontaram alguns erros notórios.

Basicamente, é conectar a subclasse corretamente em Javascript. Quando subclassificamos, temos que fazer algumas coisas divertidas para garantir que a delegação prototypal funcione corretamente, inclusive sobrescrevendo um objeto de prototype . Sobrescrever um objeto de prototype inclui o constructor , então precisamos corrigir a referência.

Vamos rapidamente ver como funcionam as 'classes' no ES5.

Digamos que você tenha uma função de construtor e seu protótipo:

//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 você chama o construtor para instanciar, diga Adam :

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

A new palavra-chave invocada com 'Person' basicamente executará o construtor Person com algumas linhas adicionais de código:

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 nós console.log(adam.species) , a pesquisa falhará na instância de adam e procuraremos a cadeia de protótipos em seu .prototype , que é Person.prototype - e Person.prototype tem uma propriedade .species , portanto, a pesquisa terá sucesso no Person.prototype . Então, ele registrará 'human' .

Aqui, o Person.prototype.constructor corretamente para Person .

Então agora a parte interessante, a chamada "subclassificação". Se quisermos criar uma classe Student , que é uma subclasse da classe Person com algumas alterações adicionais, precisaremos ter certeza de que o Student.prototype.constructor aponte para Student para precisão.

Não faz isso por si só. Quando você subclasse, o código se parece com isto:

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

Chamar new Student() aqui retornaria um objeto com todas as propriedades que queremos. Aqui, se nós verificássemos a eve instanceof Person , ele retornaria false . Se tentarmos acessar o eve.species , ele retornará undefined .

Em outras palavras, precisamos conectar a delegação para que a eve instanceof Person retorne true e, assim, as instâncias de Student Person.prototype corretamente para Student.prototype e, em seguida, Person.prototype .

MAS, como estamos chamando-o com a new palavra-chave, lembre-se do que essa invocação adiciona? Ele chamaria Object.create(Student.prototype) , que é como configuramos esse relacionamento de delegação entre Student e Student.prototype . Observe que, neste momento, o Student.prototype está vazio. Então, olhando para cima .species uma instância de Student falharia, pois ele delega apenas para Student.prototype , e a propriedade .species não existe em Student.prototype .

Quando atribuímos Student.prototype a Object.create(Person.prototype) , Student.prototype propriamente dito, delega a Person.prototype , e procurar eve.species retornará human conforme o esperado. Presumivelmente, gostaríamos que herdasse de Student.prototype AND Person.prototype. Então, precisamos consertar tudo isso.

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

Agora a delegação funciona, mas sobrescrevemos o Student.prototype com um Person.prototype . Portanto, se chamarmos Student.prototype.constructor , isso Student.prototype.constructor Person vez de Student . É por isso que precisamos consertar isso.

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

No ES5, nossa propriedade constructor é uma referência que se refere a uma função que escrevemos com a intenção de ser um 'construtor'. Além do que a new palavra-chave nos dá, o construtor é uma função 'simples'.

No ES6, o constructor está agora embutido na maneira como escrevemos as classes - como em, é fornecido como um método quando declaramos uma classe. Isso é simplesmente açúcar sintático, mas nos dá algumas conveniências como o acesso a um super quando estamos estendendo uma classe existente. Então, nós escreveríamos o código acima assim:

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




inheritance