Monthly Archives: Fevereiro 2016

Javascript orientado a objetos – II

No  artigo anterior vimos como criar classes em JS, incluindo propriedades e métodos públicos, privilegiados e privados. Este artigo explica a herança em Javascript.

Sumário

  • É possível estabelecer uma relação de herança entre classes com NomeCLasseFilha.prototype = new ClassePai();.
  • É necessário fazer reset da propriedade constructor usando NomeClasseFilha.prototype.constructor=NomeClasseFilha.
  • Podem invocar-se métodos das classes ancestrais que a classe filha tenha modificado usando o método Function.call().
  • O Javascript não tem suporte para métodos protegidos.

Exemplo

Segue-se um exemplo de herança entre duas classes:

function Mamifero(nome){
	this.nome=nome;
	this.descendencia=[];
}
Mamifero.prototype.temUmFilho=function(){
	var novoFilho=new Mamifero("Filho "+this.nome);
	this.descendencia.push(novoFilho);
	return novoFilho;
}
Mamifero.prototype.toString=function(){
	return '[Mamifero "'+this.nome+'"]';
}


Gato.prototype = new Mamifero();       // É aqui que a herança acontece
Gato.prototype.constructor=Gato;       // Caso contrário as instâncias de Gato usariam o construtor de Mamifero
function Gato(nome){
	this.nome=nome;
}
Gato.prototype.toString=function(){
	return '[Gato "'+this.nome+'"]';
}


var umAnimal = new Mamifero('Sr. Biggles');
var bicho = new Gato('Felix');
alert('umAnimal is '+umAnimal);        // resulta em 'umAnimal é [Mamifero "Sr. Biggles"]'
alert('bicho is '+bicho);              // resulta em 'bicho é [Gato "Felix"]'

bicho.temUmFilho();                    // chama um método herdado de Mamifero
alert(bicho.descendencia.length);      // mostra que o gato agora tem um filho
alert(bicho.descendencia[0]);          // resulta em '[Mamifero "Filho Felix"]'

Utilização da propriedade .constructor

Atente-se na última linha do exemplo anterior. O filho de um Gato deve ser um Gato, certo? Embora o método temUmFilho() funcione, esse método cria especificamente um new Mamifero. Embora pudéssemos criar um novo método temUmFilho() para a subclasse Gato como this.descendencia.push(new Gato("Filho "+this.nome)), seria preferível que a classe ancestral criasse um objeto do tipo correto.

Qualquer instância ou objeto em JS tem uma propriedade chamada constructor que aponta para a sua classe pai. Por exemplo, umAnimal.constructor==Mamifero é verdade. Com este conhecimento, podemos rescrever o método temUmFilho() assim:

Mamifero.prototype.temUmFilho=function(){
	var novoFilho=new this.constructor("Filho "+this.nome);
	this.descendencia.push(novoFilho);
	return novoFilho;
}
...
bicho.temUmFilho();                    // O mesmo que antes: chama o método herdado de Mamifero
alert(bicho.descendencia[0]);          // Agora resulta em '[Gato "Filho Felix"]'

Invocar ‘super’ métodos

Vamos estender o exemplo para quando os gatinhos filhos são criados, eles ‘miam’ logo assim que nascem. Para isso, vai ser necessário escrever um método próprio Gato.prototype.temUmFilho(), que seja capaz de invocar o método original Mamifero.prototype.temUmFilho():

Gato.prototype.temUmFilho=function(){
	Mamifero.prototype.temUmFilho.call(this);
	alert("miau!");
}

O código acima pode parecer bizarro. O Javascript não tem a prorpiedade ‘super’, que aponte para a classe pai. Ao invés, usa-se o método call() de uma Function do objeto, que permite invocar uma função usando um objeto diferente como contexto para essa função. Se for necessário passar parâmetros para essa função, vão após o ‘this’. Para mais informações sobre o método Function.call(), ver MSDN docs for call().

Criar uma propriedade ‘super’

Em vez de ter que saber que Gato herda de Mamifero, e ter que escrever Mamifero.prototype de cada vez que quisermos invocar um método ancestral, não seria mais interessante ter uma propriedade de gato para a sua superclasse? Os mais familiarizados com linguagens orientadas a objetos seriam tentados a chamar a essa propriedade ‘super’, mas o JS reserva esta palavra para uso futuro. A palavra ‘parent’, embora seja usada nalguns itens do DOM, pode ser usada livremente em JS, portanto vamos usar parent neste exemplo:

Gato.prototype = new Mamifero();
Gato.prototype.constructor=Gato;
Gato.prototype.parent = Mamifero.prototype;
...
Gato.prototype.temUmFilho=function(){
	var osFilhos = this.parent.temUmFilho.call(this);
	alert("miau!");
	return osFilhos;
}

Falsas classes virtuais puras

Algumas linguagens de programação orientadas a objetos têm o conceito de classe virtual pura, que é uma classe que não pode ser instanciada, mas apenas herdada. Por exemplo, podemos ter a classe CoisaViva da qual a classe Mamifero herda, mas não queremos que seja possível instanciar CoisaViva sem especificar o tipo de coisa. É possível fazer isto em JS criando a classe virtual a partir de um objeto ao invés de uma função.

O exemplo seguinte mostra como isto pode ser usado para simular uma superclasse virtual pura:

CoisaViva = {
	nascer : function(){
		this.vivo=true;
	}
}
...
Mamifero.prototype = CoisaViva;
Mamifero.prototype.parent = CoisaViva;   //Nota: não 'CoisaViva.prototype'
Mamifero.prototype.temUmFilho=function(){
	this.parent.nascer.call(this);
	var novoFilho=new this.constructor("Filho "+this.nome);
	this.descendencia.push(novoFilho);
	return novoFilho;
}

Com o código acima,, a instrução seguinte var espirito = new CoisaViva() daria erro, pois CoisaViva não é uma função, e por isso não pode ser usada como um construtor.

Herança conveniente

Ao invés de escrever 3 linhas de cada vez que queremos que uma classe herde de outra, é mais conveniente esteder o objeto Function para fazer isso por nós:

Function.prototype.herdaDe = function( objetoOuClassePai ){
	if ( objetoOuClassePai.constructor == Function )
	{
		//Herança normal
		this.prototype = new objetoOuClassePai;
		this.prototype.constructor = this;
		this.prototype.parent = objetoOuClassePai.prototype;
	}
	else
	{
		//Herança pura virtual
		this.prototype = objetoOuClassePai;
		this.prototype.constructor = this;
		this.prototype.parent = objetoOuClassePai;
	}
	return this;
}
//
//
CoisaViva = {
	beBorn : function(){
		this.vivo = true;
	}
}
//
//
function Mamifero(nome){
	this.nome=nome;
	this.descendencia=[];
}
Mamifero.herdaDe( CoisaViva );
Mamifero.prototype.temUmFilho=function(){
	this.parent.nascer.call(this);
	var novoFilho = new this.constructor( "Filho " + this.nome );
	this.descendencia.push(novoFilho);
	return novoFilho;
}
//
//
function Gato( nome ){
	this.nome=nome;
}
Gato.herdaDe( Mamifero );
Gato.prototype.temUmFilho=function(){
	var osGatos = this.parent.temUmFilho.call(this);
	alert("miau!");
	return osGatos;
}
Gato.prototype.toString=function(){
	return '[Gato "'+this.nome+'"]';
}
//
//
var felix = new Gato( "Felix" );
var gatos = felix.temUmFilho( ); // mew!
alert( kitten );                 // [Gato "Filho Felix"]

Este método deve ser chamado imediatamente após o construtor, e antes de estender o protótipo para o objeto.

Métodos protegidos?

Algumas linguagens de programação orientadas a objetos têm o conceito de métodos protegidos — métodos que existem numa classe pai ou ancestral e que só podem ser invocados por descendentes do objeto, mas não por objetos externos. O JS não suporta estas funcionalidades. Quem precisar disto, terá que escrever a sua própria framework, garantindo que cada classe tem um ‘parent’ ou uma propriedade semelhante, e sobe a árvore à procura de ancestrais, verificando se o objeto invocador é do mesmo tipo. Fazível, mas não agradável.

Traduzido de OOP in JS – 2

Javascript orientado a objetos – I

JS orientado a objetos, Parte 1 : variáveis e métodos públicos e privados

Neste artigo mostramos como criar variáveis e métodos públicos e privados em classes de Javascript.

Sumário

  • variáveis privadas são declaradas com a palavra reservada ‘var’ dentro do objeto, e só podem ser acedidas por funções privadas e métodos privilegiados.
  • funções privadas são declaradas em linha dentro do construtor do objeto (ou alternativamente podem ser declaradas via var nomeFuncao=function(){...}) e só podem ser chamadas por métodos privilegiados (incluindo o construtor do objeto).
  • métodos privilegiados são declarados com this.nomeMetodo=function(){...} e podem ser invocados por código externo ao objeto.
  • propriedades públicas são declaradas com this.nomeVariavel e podem ser lidas e escritas de fora do objeto.
  • métodos públicos são definidos por NomeClasse.prototype.nomeMetodo = function(){...} e podem ser invocados de fora do objeto.
  • propriedades do protótipo são definidas por NomeClasse.prototype.nomePropriedade = valor
  • propriedades estáticas são definidas por NomeClasse.nomePropriedade = valor

Exemplo

Neste exemplo, o nome e cor do cabelo da pessoa são definidos no início e não podem ser alterados. Uma pessoa é criada no ano 1 e com uma idade máxima escondida. A pessoa tem um peso que é modificado pelo que come (triplicando o seu peso) ou pelo exercício (diminuindo para metade). De cada vez que a pessoa come ou exercita, a sua idade aumenta uma ano. O objeto pessoa tem uma propriedade ‘roupa’ que qualquer um pode modificar, assim como um fatorLixo que qualquer um pode modificar manualmente, mas que cresce sempre que a pessoa come ou exercita, e é reduzido pelo uso do método chuveiro().

Código exemplo

function Pessoa(n,cabelo){
	this.constructor.populacao++;

	// ************************************************************************
	// Variáveis e funções privadas
	// só me´todos privilegiados podem ver/editar/invocar
	// ***********************************************************************
	var vivo=true, idade=1;
	var maxIdade=70+Math.round(Math.random()*15)+Math.round(Math.random()*15);
	function maisVelho(){ return vivo = (++idade <= maxIdade) }

	var meuNome=n?n:"Maria João";
	var peso=1;


	// ************************************************************************
	// Métodos privilegiados
	// Podem ser invocados publicamente e aceder a itens privados
	// não podem ser alterados; podem ser substituídos por versões públicas
	// ************************************************************************
	this.toString=this.getNome=function(){ return meuNome }

	this.comer=function(){
		if (maisVelho()){
			this.fatorLixo++;
			return peso*=3;
		} else alert(meuNome+" não pode comer, morreu!");
	}
	this.exercicio=function(){
		if (maisVelho()){
			this.fatorLixo++;
			return peso/=2;
		} else alert(meuNome+" não pode comer, morreu!");
	}
	this.pes=function(){ return peso; }
	this.getCor=function(){ return cor; }
	this.getIdade=function(){ return idade; }
	this.muitoTempo=function(){ idade+=50; this.fatorLixo=10; }


	// ************************************************************************
	// Propriedades públicas -- Qualquer um pode ler/escrever
	// ************************************************************************
	this.roupa="nada/nú";
	this.fatorLixo=0;
}


// ************************************************************************
// Métodos públicos -- Qualquer um pode ler/escrever
// ************************************************************************
Person.prototype.serFixe = function(){ this.roupa="cáqui e camisa preta" }
Person.prototype.chuveiro = function(){ this.fatorLixo=2 }
Person.prototype.mostraPernas = function(){ alert(this+" tem "+this.pernas+" pernas") }
Person.prototype.amputar = function(){ this.pernas-- }


// ************************************************************************
// Propriedades do protótipo -- Qualquer um pode ler/Escrever (mas podem ser reescritas)
// ************************************************************************
Person.prototype.pernas=2;


// ************************************************************************
// Propriedades estáticas -- Qualquer um pode ler/Escrever
// ************************************************************************
Person.populacao = 0;



// Aqui vai o código que usa a classe Pessoa
function GereVidaDoAlex(){
	var gk=new Pessoa("Alex","castanho");       //Nova instância da classe Pessoa
	var lk=new Pessoa("Maria","ruiva");         //Nova instância da classe Pessoa
	alert("Neste momento há "+Pessoa.populacao+" pessoas");

	gk.mostraPernas(); lk.mostraPernas();                 //Ambos partilham a variável comum 'Pessoa.prototype.pernas' quando olham para 'this.legs'

	gk.cabelo = "castanho";                               //afeta uma variável pública, mas não reescreve a variável 'cor'.
	alert("A cor do cabelo de "+gk+" é "+gk.getCor());    //Devolve 'castanho' da variável privada 'cor' criada no início..
	gk.comer(); gk.comer(); gk.comer();                   //o peso é 3...depois 9...depois 27
	alert(gk+" pesa "+gk.peso()+" quilos e tem um fator lixo de "+gk.fatorLixo);

	gk.exercicio();                                // o peso é agora 13.5
	gk.serFixe();                                  //a roupa foi atualizada para a moda atual
	gk.roupa="Roupa de cafetão";                   //A roupa é uma variável pública que pode ser atualizada para qualquer valor
	gk.chuveiro();
	alert("A atual tecnologia de chuveiro obteve "+gk+" para um fator lixo de "+gk.fator Lixo);

	gk.muitoTempo();                              //Passam 50 anos
	Person.prototype.chuveiro=function(){         //A tecnologia de chuveiro melhorou para todos
		this.fatorLixo=0;
	}
	gk.serFixe=function(){                        //O Alex tem novas ideias sobre moda
		this.roupa="Roupa de lata";
	};

	gk.serFixe(); gk.chuveiro();
	alert("O elegante "+gk+" com "
		+gk.getIdade()+" anos, veste agora "
		+gk.roupa+" com fator lixo "
		+gk.fatorLixo);

	gk.amputar();                                 //Usa a propriedade do protótipo e cria uma propriedade pública
	gk.mostraPernas(); lk.mostraPernas();         //A Lisa ainda tem a propriedade do protótipo

	gk.muitoTempo();                              //Passm 50 anos...O Alex tem agora 100 anos
	gk.comer();                                   //Queixa-se sobre idade extrema, morte e dificuldade em comer.
}

Notas

  • maxIdade é uma variável privada, sem método de acesso privilegiado; ou seja, não há forma pública de alterá-la ou consultá-la.
  • cabelo é uma variável privada definida como argumento para o construtor. Variáveis passadas para o construtor estão disponíveis no objeto como variáveis privadas.
  • O método serFixe() 'Roupa de Lata' foi aplicado apenas ao objeto gk, e não a toda a classe Pessoa. Para outras pessoas, o método serFixe() usa a roupa original ‘cáqui e camisa preta’ que o Alex abandonou mais tarde.
  • Notar a chamada implícita ao métido gk.toString() quando se usa a concatenação de strings. É isto que permite que o código alert(gk+' é fixe.') coloque o nome ‘Alex’ lá, e é equivalente a alert(gk.toString()+' é fixe.'). Qualquer objeto de qualquer tipo em JS tem um método .toString(), mas pode ser substituído por um nosso.
  • Não é possível (em princípio) definir métodos públicos de uma classe dentro do seu construtor.  É necessário usar a propriedade prototype externamente, como nos métodos serFixe() e chuveiro() acima.
  • Como tentei mostrar com a propriedade Pessoa.prototype.pernas e a função amputate(), propriedades do protótiplo são partilhadas por todas as instâncias ou objetos. A propriedade lk.pernas tem o valor 2. Contudo, tentar alterar este valor usando gk.pernas=1 ou (no objeto Pessoa) this.pernas=1 acaba por criar uma nova propriedade pública específica para essa instância. (É por isso que invocar gk.amputar() só remove uma perna ao Alex, mas não a Lisa.) Para alterar uma propriedade do protótipo, devemos usar Pessoa.prototype.pernas=1 ou algo como this.constructor.prototype.pernas=1. (digo “algo como” porque this.constructor não está disponível dentro de funções privadas do objeto, uma vez que this se refere ao objeto todo.)
  • Sempre que uma função anónima é declarada em linha com
    foo = function(p1,p2){ código }
    o construtor new Function() NÃO é equivalente, por exemplo, a
    foo = new Function('p1','p2','código');
    uma vez que o último corre no âmbito global – ao invés de herdar o contexto da função construtor – não permitindo assim que ele aceda às variáveis privadas.
  • Como indicado nos comentários acima, o ato de afetar gk.cabelo com um valor qualquer NÃO substitui a variável privada cabelo. Embora seja uma ideia parva, é possível ter ambas as variáveis, pública e privada, com o mesmo nome. Por exemplo, o método  gritat() na classe seguinte, tem valores diferentes para foo e this.foo:
    function ClasseParva(){
      var foo = "interno";
      this.foo = "externo";
      this.gritar=function(){ alert("O foo interno é "+foo+"\nO foo externo é "+this.foo) }
    }
  • Funções privadas e métodos privilegiados, tal como variáveis privadas e propriedades públicas, são instanciados com cada novo objeto criado. Por isso, de cada vez que se cria uma pessoa com new Pessoa() são criadas novas cópias de maisVelho(), toString(), getNome(), comer(), exercício(), peso(), getCor(), getIdade(), and muitoTempo(). Para cada Pessoa, de cada vez. Isto contrasta com os métodos públicos (existe apenas uma cópia de serFixe() e chuveiro() independentemente do número de objetos de Pessoa que sejam criados) e por motivos de consumo de memória e desempenho é preferível abandonar algum grau de proteção dos objetos a favor do uso de métodos públicos. Note-se que, para isso, é necessário tornar públicas as variáveis privadas (uma vez que sem métodos de acesso privilegiado não haveria maneira de usá-las) para que os métodos públicos sejam capazes de ter acesso a elas; o que também permite que código externos veja e destrua essas variáveis. A otimização de memória e desempenho, recorrendo ao uso exclusivo de propriedades públicas, tem consequências que podem tornar o código menos robusto. Por exemplo, no código acima, as variáveis idade e maxIdade são privadas; idade só pode ser acedida externamente através de getIdade() (não pode ser modificada) e maxIdade não pode ser lida nem modificada externamente. Torná-las públicas iria permitir que qualquer código executasse gk.maxIdade=1; gk.idade=200; o que não faz sentido (não deve ser possível manipular a idade ou a esperança de vida de alguém diretamente), e como consequência, a variável vivo não poderia ser atualizada, deixando o objeto Pessoa num estado incoerente.
Traduzido de OOP in JS