"Se não queres que ninguém saiba, não o faças." - Provérbio Chinês
Construtores possuem uma certa peculiaridade em JavaScript. Ao contrário de outras linguagens, JS lhe permite moldar à vontade a maneira de construir objetos. Você pode, para citar um exemplo, criar uma nova instância de uma classe sem nem sequer fazer uso da palavra reservada new. Para isso, entretanto, é necessário conhecer o mecanismo interno de construtores e é isso que veremos aqui, dividido em duas partes:
- Contrutores, O Que São e Como Criá-los. Essa seção tem caráter mais introdutório e programadores já habituados a ele podem passá-la sem problemas.
- O segredo dos JavaScript Constructors. Essa é a seção principal do post que visa a demonstrar o mecanismo interno de construtores em JavaScript com ênfase no papéis exercidos pelas keywords this e new.
Contrutores, O Que São e Como Criá-los
Construtores possuem um padrão bastante conhecido no mundo do desenvolvimento, pois utilizam a palavra reservada new antes da criação de uma nova instância. Se você vê a palavra new antes da chamada de uma função, você imediatamente sabe o que está acontecendo, estamos criando uma nova instância de uma determinada classe. Mas o que significa criar uma nova instância, o que significa construir um objeto?
Construir um objeto é conferir a ele as características ou propriedades necessárias para que ele tenha o comportamento esperado, isto é, para que ele seja capaz de realizar todas as ações que se espera de um elemento de sua classe. Assim, digamos que você queira construir pessoas cujas únicas capacidades são dizer seu nome, dizer sua idade e envelhecer. De que você precisa para isso? Apenas de duas propriedades, a idade e o nome, vejamos a implementação:
function Person(name,age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log("My name is: " + this.name);
};
this.sayAge = function() {
console.log("My age is: " + this.name);
};
this.growOld = function() {
this.age += 1;
};
}
var johnDoe = new Person("John Doe", 44);
johnDoe.sayName();johnDoe.sayAge();
johnDoe.growOld();johnDoe.sayAge();
var notAPerson = new Person();
notAPerson.sayName();notAPerson.sayAge();
notAPerson.growOld();notAPerson.sayAge();
O exemplo acima mostra bem o que queremos dizer por "construir um objeto com comportamento esperado". Nele, johnDoe é de fato uma pessoa capaz de falar e envelhecer, enquanto a notAPerson, apesar de ser uma instância da classe Pessoa, não é capaz de falar nem de envelhecer, pois foi mal construído. Ao objeto notAPerson, não foram atribuídas as características que de fato o tornariam uma pessoa.
O Segredo dos Javascript Constructors
Em outras linguagens, quando utilizamos a keyword new criamos uma nova instância de uma classe e nos damos por satisfeitos mesmo ignorando o mecanismo oculto. Em JavaScript, pelo poder que nos é concedido de alterar à vontade o mecanismo de construtores, não podemos nos dar por satisfeitos em simplesmente usá-los. Nós temos também que conhecer seus segredos.
Quando o códigovar johnDoe = new Person(name)
é executado, acontece o seguinte:
- 1 - Um novo objeto é criado herdando de Person.prototype;
var johnDoe = Object.create(Person.prototype);
- 2 - A função construtora Person é executada com os argumentos especificados e this assume o valor do objeto que acabou de ser criado.
johnDoe = Person.call(johnDoe, name);
- 3 - Entretanto, a chamada do método call no passo 2 acontece de uma maneira peculiar. Quando não for especificado nenhum retorno na função construtora, ela retornará o próprio objeto this. Quando houver um retorno explícito o objeto criado no passo 1 será sobrescrito pelo resultado.
Vejamos o que queremos dizer, através do exemplo a seguir:
//demonstrando o mecanismo normal
function Person(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}
Person.prototype.sayHey = function(){
console.log("hey");
}
var johnNew = new Person("John New");
johnNew.sayName(); // John New
console.log('sayHey' in johnNew); //true:herda de Person.prototype
/////////////////
//demonstrando o mecanismo explicitamente
function PersonThis(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
return this;
}
PersonThis.prototype.sayHey = function(){
console.log("hey");
}
var john = Object.create(PersonThis.prototype);
john = PersonThis.call(john, "John");
john.sayName(); // John
console.log('sayHey' in john); //true:herda de PersonThis.prototype
///////////
//demonstrando que podemos escolher o retorno
function PersonOverride(name) {
this.name = name;
this.sayName = function() {
alert("My name is: " + this.name);
};
var a = {};
a.name = name;
return a;
}
PersonOverride.prototype.sayHey = function(){
console.log("hey");
}
var johnSobrescrito = new PersonOverride("John Sobrescrito");
console.log(johnSobrescrito.name); //John Sobrescrito
//false: não herda de PersonOverride.prototype
console.log('sayHey' in johnSobrescrito);
//false: não possui a propriedade sayName
console.log('sayName' in johnSobrescrito);
Para finalizar o raciocínio desenvolvido no exemplo, experimente a mundança a seguir no construtor PersonOverride. Consegue prever o que vai acontecer?
function PersonOverride(name) {
this.name = name;
this.sayName = function() {
alert("My name is: " + this.name);
};
var a = "inesperado";
return a;
}
var johnSobrescrito = new PersonOverride("John Sobrescrito");
console.log(johnSobrescrito.name);
Construtores Sem "new"
Diante do que foi exposto até aqui, podemos atacar outro problema que é razoavelmente comum encontrar em JavaScript, criação de instâncias sem a keyword new.
Imagine por um instante o que aconteceria se chamássemos a função construtora Person sem a palavra-chave new. O que estariamos fazendo?
var john = Person("John");
console.log(window.name); //John
Estaríamos adicionando propriedades ao escopo global, posto que esse seria o valor que this assumiria e isso é perigoso. Como fazer para contornar isso, como conferir à função a inteligência de saber se está sendo chamada com ou sem o new?
function Person(name) {
//se chamada com o new
if (this instanceof Person) {
this.name = name;
} else {
return new Person(name);
}
}
var person1 = new Person("John");
var person2 = Person("John");
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Person); //true
Reaproveitando Construtores
Criamos a classe Person cujas instâncias representam pessoas. Digamos agora que queremos criar objetos que representem profissionais e a única propriedade adicional que eles possuem em relação às pessoas é a profissão.
Pensando em termos de herança, podemos dizer que profissionais são pessoas, isto é, profissionais são capazes de substituir pessoas, simular todas suas ações(Liskov Substitution Principle). Por conta dessa relação de herança, podemos simplificar a construção de profissionais, reaproveitando a chamada do construtor Person das pessoas, conforme abaixo:
function Person(name,age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
};
this.sayAge = function() {
console.log(this.age);
};
}
function Professional (name ,age, job) {
this.job = job;
this.sayJob = function(){ console.log(this.job); };
// Reaproveitamento do construtor da classe pai
Person.call(this, name, age);
}
var john = new Professional("John", 44, "Developer");
john.sayJob(); // Developer
john.sayName(); // John
john.sayAge(); // 44
Curiosidades
- Construtores em JavaScript são por convenção declarados com a primeira letra maiúscula para se diferenciar das demais funções, porém essa boa prática não é obrigatória.
- Uma prática pouco comum, mas possível é criar instâncias sem a abertura de parênteses, neste caso new Person é o mesmo que new Person().
Próximos Passos
Os exemplos dados neste post possuem uma deficiência. Toda vez que construímos uma nova pessoa, usando o construtor Person, estamos criando em todas as instâncias as propriedades sayName, sayAge e growOld. Isso aparentemente é insignificativo, mas pode acarretar em uso excessivo e desnecessário de memória.
Melhor seria se fossemos capaz de centralizar os métodos dos objetos Person em um só lugar e fazer referência a eles quando necessário. Como fazer isso?
É aí que o conhecimento de construtores e protótipos em JavaScript cai como uma luva:
- Aprenda Constructors e Prototypes em JavaScript