De volta ao básico: herança prototípica

A herança com sabor de JavaScript difere um pouco da programação tradicional baseada em OOP. Embora agora tenhamos uma classpalavra-chave no ES6 para fingir que o JavaScript pode usar um disfarce como OOP, ainda é uma herança baseada em protótipo sob o capô.

Herdado vs. Possuído

Os objetos têm propriedades (o método é um tipo especial de propriedade cujo valor é uma função) e essas propriedades podem ser definidas no próprio objeto ou herdadas de sua cadeia de protótipo.

Quando uma propriedade não é encontrada no próprio objeto, o mecanismo JavaScript inspeciona a cadeia de protótipos para encontrar a referida propriedade. Uma propriedade é considerada não encontrada se não estiver disponível em nenhum protótipo (ou link) na cadeia de protótipo. Portanto, se uma propriedade for encontrada em um elo anterior da cadeia, ela irá sombrear a mesma propriedade encontrada no último elo da cadeia.

Em certo sentido, a herança do JavaScript é encontrar propriedades compartilhadas disponíveis em uma trilha de navegação comum.

Existem algumas maneiras de herdar de um objeto em JavaScript:

  1. Usando a função para retornar a você um objeto base. Todas as propriedades que você deseja herdar já estão no objeto base. Mas, em vez de herdá-lo, você está fazendo uma cópia dele para usar como parte desse objeto.
  2. Usando para configurar como o primeiro elo na cadeia de protótipo. O objeto criado terá propriedade (dependendo do mecanismo JavaScript, essa propriedade pode não estar disponível para você, e o ES6 também disse que esta propriedade não deveria estar disponível para você)Object.create(baseObject)baseObject__proto__
  3. Usando o construtor e a cadeia de protótipos, este é o estilo de herança mais comum ou de livro-texto que você verá e também é o tópico principal deste post.

Construtor e Protótipo

Se você ainda não sabe disso, o construtor manipula thispara criar propriedades pertencentes à instância e o protótipo manipula __proto__para criar propriedades compartilhadas por instância. Essa é a maior diferença entre eles.

A instância criada terá duas propriedades especiais, e é bastante interessante porque a herança é indicada não por uma, mas por duas propriedades:

  • constructor, usado para indicar de qual construtor foi criado
  • __proto__, primeiro elo da cadeia de protótipo. Por ser uma corrente, para chegar ao segundo elo, tudo o que você precisa fazer é continuar adicionando __proto__propriedades ao caminho até chegar null. Em outras palavras, se você fosse escrever uma função de pesquisa, provavelmente seria assim:
function lookupPrototypeChain(obj, prop) {
// this is not an object, so it's not a valid lookup
if (obj !== Object(obj)) return undefined;

while (obj.__proto__ !== null) {
if (obj.hasOwnProperty(prop)) {
return obj[prop];
}

// lookup the next item in the chain
obj
= obj.__proto__;
}

// nothing is found, return undefined explicitly
return undefined;
}

__proto__ vs. prototype

Antes de pular para um exemplo, vamos dar uma olhada na diferença entre __proto__e prototype.

__proto__propriedade está disponível em qualquer objeto JavaScript para obter sua cadeia de protótipo, enquanto prototypeé uma propriedade na função do construtor que é usada para gerar o __proto__objeto para todas as instâncias criadas pela função do construtor.

Depois que isso for esclarecido, vamos dar uma olhada em um exemplo de herança usando construtor e protótipo:

function A() {}
A
.prototype.isBored = function() {
return Math.random() > 0.5;
};

function B() {}
// B gets A's prototype as its `__proto__`
B
.prototype = Object.create(A.prototype);
B
.prototype.constructor = B;
B
.prototype.haveFun = function() {
console
.log('Hooray!');
};

var b = new B();

// { constructor, __proto__, haveFun }, B's prototype
console
.log(b.__proto__);

// constructor is function B, is the same as b.__proto__.constructor
// it's basically saying you don't have `constructor` property
// but your prototype chain does, so let's return that
b
.constructor === B;

// makes sense, huh?
b
.constructor.prototype === B.prototype;

// constructor of any function is the top-level Function itself.
// You don't get A here because prototype chain doesn't live on `constructor` property, it lives on `__proto__` property
b
.constructor.constructor === Function;

// { constructor, __proto__, isBored }, A's prototype
console
.log(b.__proto__.__proto__);

// next link on the prototype chain is A
// again this is the same as b.__proto__.__proto__.constructor
b
.__proto__.constructor === A;
b
.__proto__.constructor.prototype = A.prototype;

// Again, this is top-level Function
b
.__proto__.constructor.constructor === b.__proto__.constructor;

Construtor e instanciação

Como o construtor é apenas uma função que você pode invocar diretamente, é possível apenas chamá-lo para avaliar. No entanto, a ressalva é que pode envolver a chamada thisdentro da função do construtor. No modo estrito, o mecanismo JavaScript resultaria em um erro ao executar a função, portanto, tome cuidado com seu uso.

Além de invocar diretamente, o uso mais comum do construtor é chamá-lo usando o newoperador. Aqui está o que StackOverflow diz sobre o processo http://stackoverflow.com/questions/1646698/what-is-the-new-keyword-in-javascript

Resumindo, o processo é assim: o __proto__objeto é gerado prototypeprimeiro, vincula thisse à instância atual, executa a função do construtor e retorna. Isso significa algumas coisas:

  1. Dentro da função construtora, thissó pode se referir a propriedades no objeto de protótipo. Você não pode chamar propriedades pertencentes fora da função do construtor porque ela ainda não foi declarada.

  2. Mesmo se você não especificar uma prototypepropriedade, uma será adicionada automaticamente ao criar uma instância.

  3. thisos prototypemétodos internos e a função do construtor referem-se à instância, não ao prototypeobjeto ou à função do construtor. Então, se você quiser se referir ao próprio objeto de protótipo dentro de um método de protótipo, você pode se referir a ele como ouConstructor.prototypethis.constructor.prototype

Mudando e quebrando coisas

Se você modificou a prototypefunção depois de instanciar, as duas instâncias compartilhariam suas propriedades, mas são diferentes. A razão é que em sua cadeia de protótipo, eles estão apontando para o mesmo objeto ( ) do qual é gerado a partir de enquanto é um instantâneo do que parece no momento da instanciação..constructor.prototype.__proto__.prototype__proto____proto__prototype

Aqui está um exemplo:

function A() {}
A
.prototype.sayYo = function() {
console
.log('yo');
};

var a1 = new A();

A
.prototype.sayHo = function() {
console
.log('ho');
};

var a2 = new A();

a1
.constructor.prototype === a2.constructor.prototype;
a1
.__proto__ === a2.__proto__;

Se você modificou a função do construtor depois de instanciar, você precisa garantir que o protótipo seja definido novamente. Caso contrário, você está substituindo a função.

instanceof operador

Em JavaScript, você pode usar o instanceofoperador para verificar se um objeto é uma instância de um construtor.

Um objeto de função tem um método interno chamado HasInstanceque pega um valor e compara seu protótipo com o protótipo da função (ou construtor). Se você for escrever instanceofsozinho, você acabará com algo assim

function isInstanceOf(func, val) {
if (val !== Object(val)) return false;

while (val.constructor.prototype !== null) {
if (val.constructor.prototype !== Object(val.constructor.prototype)) {
throw new TypeError('input object does not have a prototype chain');
}

if (val.constructor.prototype === func.prototype) {
return true;
}

val
= val.__proto__;
}

return false;
}

Herança prototípica vs. herança OOP

Aqui está um rápido resumo de suas diferenças

| Terms            | JavaScript | OOP |
|------------------|------------|-----|
| private variable | closure | `private` |
| private function | declared inside constructor function | `private` |
| public variable | this.varName | `public` |
| public function | this.methodName | `public` |
| static variable | Constructor.varName | `static` |
| static function | Constructor.prototype.methodName | `static` |

No final

Dependendo do uso de objetos, aqui está um breve resumo do que você pode fazer com cada forma de herdar um objeto

|                       | function | `Object.create` | constructor/prototype |
|-----------------------|----------|-----------------|-----------------------|
| has `constructor` | false | false | true |
| has `__proto__` | false | true | true |
| properties are shared | false | true | true |
| properties are owned | true | false | true |
| customise instance | true | false | true |

Fontes: