"Uma vez que você tenha experimentado voar, você andará pela terra com seus olhos voltados para céu, pois lá você esteve e para lá você desejará voltar." Leonardo da Vinci
Prototype é um daqueles conceitos que inicialmente podem ser difíceis, mas que, uma vez que você entende, lhe deixa tão entusiasmado com JavaScript que você não vai querer voltar pra outras linguagens. O entendimento de prototypes é essencial para compreensão do mecanismo de herança, logo para o domínio de orientação a objeto em JavaScript.
A apresentação a seguir se baseia no melhor livro que encontrei no assunto de orientação a objetos em JavaScript: The Principles of Object Oriented JavaScript do autor Nicholas Zakas. Aqui farei uso de uma ilustração do livro e do caso exemplo, porém, o assunto será tratado mais simplificadamente, indo direto aos pontos de maior interesse dos que não conhecem muito bem o conceito.
Apresentando prototypes
Em O segredo dos Javascript constructors, finalizamos com a consideração de que não é recomendável a inserção de métodos dentro de construtores por acarretar um grande e desnecessário uso de memória. Fizemos essa recomendação, mas não dissemos como solucionar o problema. A solução, como era de se esperar, está na utilização de prototypes. Vejamos o problema e a solução.
O problema - Má Implementação
Anteriormente, criamos o construtor de pessoas Person, com a definição de métodos dentro do corpo da função, conforme abaixo:
function Person(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
this.toString = function() {
return "[Person " + this.name + "]";
};
}
Como dissemos, o grande problema é que cada instância teria seu próprio método ocupando um espaço diferente na memória. Isso é facilmente constatado abaixo:
var person1 = new Person("Nicholas");
var person2 = new Person("Greg");
console.log(person1.sayName === person2.sayName); //false
Considerando que são apenas duas instâncias e dois métodos, o dano não é tão grande, mas imagine que você tivesse trabalhando com dados de toda população brasileira, cerca de 200 milhões de habitantes. Isso seriam (400.000.000 - 2) espaços desnecessários armazenados na memória.
A Solução - Boa Implementação
O uso de prototypes vem justamente pra centralizar essa referência. Vejamos como seria a implementação anterior, mas agora fazendo uso deles:
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
},
toString: function() {
return "[Person " + this.name + "]";
}
};
O resultado visual do que acabamos de fazer segue abaixo. Note que os métodos sayName e toString são agora propriedades de Person.prototype e as instâncias fazem referência a eles.
Vamos agora analisar a ilustração de diversas formas, considerando a criação das duas instâncias ilustradas:
var person1 = new Person("Nicholas");
var person2 = new Person("Greg");
1. A propriedade [[prototype]]
A propriedade __proto__
de person1 e person2, embora de uso controverso, é o bastante para mostrar que a propriedade interna [[prototype]] faz referência a Person.prototype.
console.log(person1.__proto__);
//outra maneira: Object.getPrototypeOf(person1);
/*
>Person {}
>constructor: Person(name)
>sayName: ()
>toString: ()
>__proto__: Object
*/
Outra maneira de constatar a mesma coisa seria usando o método Object.getPrototypeOf(person1);
Obs: Note que o próprio Person.prototype possui uma propriedade interna [[prototype]] que faz referência a Object.protototype. Isso é reflexo do mecanismo de herança do JavaScript que veremos em detalhes no próximo artigo.
2. Referência em Cadeia
Observe que as instâncias person1 e person2 não possuem mais as propriedades sayName e toString, mas ainda assim podemos invocar os métodos, através desses objetos. Isso acontece, porque em JavaScript existe uma busca em cadeia por propriedades. Assim, quando person1 executa sayName, ocorre uma busca por esse método primeiramente no próprio objeto e, em seguida, em Person.prototype que é de quem a instância herda. Se não fosse encontrado o método em Person.Prototype seria feita uma busca em Object.prototype que é a última referência da cadeia. Vejamos como podemos demonstrar isso, em termos de código:
console.log( person1.hasOwnProperty("sayName") ); // false
console.log( Person.prototype.hasOwnProperty("sayName") ); // true
console.log( "sayName" in person1 ); //true
console.log( Person.prototype.hasOwnProperty("hasOwnProperty") ); // false
console.log( Object.prototype.hasOwnProperty("hasOwnProperty") ); // true
O método hasOwnProperty verifica se o parâmetro passado é uma propriedade inerente ao objeto. Como não é o caso para sayName, o retorno é false. Observe que o próprio método hasOwnProperty não pertence a person1, tampouco a Person.prototype, mas mesmo assim podemos invocá-lo por conta do mecanismo de referência em cadeia citado.
A expressão in, em "sayName" in person1, verifica se a propriedade existe diretamente no objeto ou em sua cadeia de prototypes.
3. Centralização de métodos
Uma maneira simples de demonstrar que de fato a referência dos métodos está centralizada nos prototypes é verificar o código abaixo no console:
console.log( Person.prototype.hasOwnProperty("sayName") ); // true
console.log(person1.sayName === person2.sayName); //true
4. A Propriedade Constructor
A função construtora Person é automaticamente atribuída à propriedade constructor de Person.prototype no momento de sua declaração. Verifique:
function Person(name) {
this.name = name;
}
console.log( Person.prototype.hasOwnProperty("constructor") ); // true
console.log( Person.prototype.constructor === Person ); // true
var person1 = new Person("Nicholas");
console.log( person1.constructor === Person.prototype.constructor ); //true
Outro Padrão de Prototypes
Existe um outro padrão de definição de prototypes que repete Person.prototype em todas as declarações de métodos ou propriedades. Para evitar essa repetição, utilizamos o seguinte padrão:
Person.prototype = {
sayName: function() {
console.log(this.name);
},
toString: function() {
return "[Person " + this.name + "]";
}
};
É preciso, entretanto, cautela, pois ao fazermos isso estamos sobrescrevendo completamente o objeto Person.prototype antes automaticamente criado. O resultado é que agora Person.prototype não possui mais a propriedade constructor apontando para Person:
console.log( Person.prototype.hasOwnProperty("constructor") ); // false
console.log( Person.prototype.constructor === Person ); // false
A melhor maneira de utilizarmos o padrão acima sugerido é explicitamente criar a propriedade constructor e apontá-la para a função construtora. Desse modo, retornamos ao comportamento esperado. E foi exatamente isso que fizemos no começo desse artigo, mas tenho certeza que passou despercebido pela maioria que está iniciando em JavaScript.
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
},
toString: function() {
return "[Person " + this.name + "]";
}
};
console.log( Person.prototype.hasOwnProperty("constructor") ); // true
console.log( Person.prototype.constructor === Person ); // true
Próximos Passos
Como foi mencionado, prototype é chave essencial do mecanismo de herança em Javascript e é justamente isso que iremos ver a seguir:
- Tipos de Herança em JavaScript