Prototypes em JavaScript

Quando se define uma função em JavaScript, esta vem com algumas predefinições, sendo uma destas o Prototype. Neste artigo vou entrar em detalhe sobre como fazer uso desta propriedade e o porquê de a utilizar nos seus projectos.

O Que é o Prototype?

A propriedade Prototype é inicialmente um objecto vazio, ao qual podemos adicionar novos membros.

Nada melhor que um exemplo para o explicar:

var meuObjecto = function($nome){
    this.nome = $nome;
    return this;
};alert(typeof meuObjecto.prototype); // objectmeuObjecto.prototype.getNome = function(){
    return this.nome;
};

No código acima criámos uma função, mas se executarmos meuObjecto(), esta só vai devolver o objecto window, isto porque foi criada dentro do global scope, sendo assim o this irá devolver o objecto global, pois ainda não instanciámos o objecto (será explicado mais tarde).

O Link Secreto

Antes de continuar a explicação, convém explicar o link secreto que faz com que o Prototype funcione desta maneira.

Todos os objectos em JavaScript têm uma propriedade “secreta” quando são definidos ou instanciados, esta propriedade chama-se de __proto__ e é com ela que se acede à corrente Prototype. De qualquer maneira, não é boa ideia aceder à propriedade __proto__ directamente, pois não está disponível em todos os browsers.

A propriedade __proto__ não deve ser confundida com o Prototype do objecto, estas são duas propriedades que andam lado a lado. É importante fazer esta distinção, pois pode criar alguma confusão ao inicio.

Explicação Entre __proto__ e Prototype

Quando criámos a função meuObjecto, nos estávamos a criar um objecto do tipo Function.

alert(typeof meuObjecto); // function

Para aqueles que não sabem, Function é  um objecto pre-definido em JavaScript, e, como resultado, tem as suas próprias propriedades (ex: length e arguments), métodos (ex: call e apply) e, como já disse antes, o seu próprio objecto Prototype assim como o link secreto __proto__. Isto significa que algures no motor de JavaScript, existe um pedaço de código similar ao seguinte:

Function.prototype = {
    arguments: null,
    length: 0,
    call: function(){
        // secret code
    },
    apply: function(){
        // secret code
    }
    ...
}

Claro que isto é apenas uma ilustração de como a corrente Prototype funciona, o verdadeiro código será alguma muito mais complexo.

Continuando, nós definimos então a nossa função meuObjecto com um argumento, $nome, mas nunca definimos as propriedades length ou métodos como o call, portanto, porque que o seguinte código funciona?

console.log(meuObjecto.length); // 1 (sendo este o numero de argumentos disponível)

Isto acontece porque, quando definimos o meuObjecto, é automaticamente criada a propriedade __proto__ e é-lhe atribuído o valor Function.prototype (ilustrado no código acima). Logo, ao aceder à propriedade meuObjecto.length, o motor de JavaScript procura pela propriedade dentro do objecto meuObjecto e se não a encontrar, viaja através da corrente através do link __proto__, encontra a propriedade e devolve-a.

Poderá estar a pensar porquê que o length está definido a 1 e não a 0 ou qualquer outro número aleatório. Isto acontece porque o meuObjecto é de facto uma instância do objecto Function.

console.log(meuObjecto instanceof Function); // true
console.log(meuObjecto === Function); // false

Quando é criada uma nova instância de um objecto, a propriedade __proto__ é actualizada para apontar para o correcto Prototype construtor, que neste caso é Function.

console.log(meuObjecto.__proto__ === Function.prototype) // true

Adicionalmente, quando criamos um novo objecto do tipo Function, o código fonte dentro do construtor Function vai contar o numero de argumentos e actualizar this.length respectivamente, que neste caso será 1.

Se, de outra forma, nós criarmos uma nova instância do objecto meuObjecto utilizando a palavra-chave new, __proto__ vai apontar para meuObjecto.prototype sendo meuObjecto o novo construtor da nossa nova instância.

var minhaInstancia = new meuObjecto(“foo”);
console.log(minhaInstancia.__proto__ === meuObjecto.prototype); // true

Agora para além de termos acesso aos métodos fonte dentro de Function.prototype, como call e apply, temos também acesso ao método do meuObjecto.getNome.

console.log(minhaInstancia.getNome()); // foo
var segundaInstancia = new meuObjecto(“bar”);console.log(segundaInstancia.getNome()); // bar
console.log(minhaInstancia.getNome()); // foo

Como podes imaginar isto poderá trazer muitos benefícios, já que podemos criar plantas base de objectos e criar quantas instâncias nos desejarmos. O que me leva ao próximo tópico.

Porque Que Usar Prototype é Melhor?

Imaginemos que estamos a desenvolver um jogo em canvas, e precisamos de alguns (talvez centenas) elementos no ecrã de uma só vez. Cada objecto necessita das suas propriedades tal como x e y, width e height, etc…

Ou seja, teríamos algo do género:

var GameObject1 = {
    x: Math.floor((Math.random() * myCanvasWidth) + 1),
    y: Math.floor((Math.random() * myCanvasHeight) + 1),
    width: 10,
    height: 10,
    draw: function(){
        myCanvasContext.fillRect(this.x, this.y, this.width, this.height);
    }
    ...
};
var GameObject2 = {
    x: Math.floor((Math.random() * myCanvasWidth) + 1),
    y: Math.floor((Math.random() * myCanvasHeight) + 1),
    width: 10,
    height: 10,
    draw: function(){
        myCanvasContext.fillRect(this.x, this.y, this.width, this.height);
    }
    ...
};

E fazer isto 97 vezes…

O que isto vai fazer é criar em memória todos estes objectos, suas propriedades, métodos,  e tudo o resto relativo às mesmas. Isto não será ideal, pois o jogo ficará lento em alguns browsers, ou parar mesmo de funcionar.

Enquanto isto poderá não acontecer com apenas 100 elementos, mesma assim pode ser um soco na performance do jogo, pois estará a correr 100 objectos em vez de apenas um objecto Prototype.

Como Utilizar Prototype?

Para fazer com que a aplicação corra mais rápida (e seguir as melhores práticas), podemos redefinir a propriedade Prototype do GameObject.

Cada instância do  objecto GameObject vai então utilizar os métodos dentro do GameObject.prototype como se eles fossem os seus próprios métodos.

// definir a função construtora do GameObject
var GameObject = function(width, height) {
    this.x = Math.floor((Math.random() * myCanvasWidth) + 1);
    this.y = Math.floor((Math.random() * myCanvasHeight) + 1);
    this.width = width;
    this.height = height;
    return this;
};// redefinir prototype do objecto GameObject
GameObject.prototype = {
    x: 0,
    y: 0,
    width: 5,
    width: 5,
    draw: function() {
        myCanvasContext.fillRect(this.x, this.y, this.width, this.height);
    }
};

Podemos então instanciar o objecto GameObject 100 vezes:

var x = 100,
arrayDeGameObjects = [];
do {
    arrayDeGameObjects.push(new GameObject(10, 10));
} while(x--);

Agora temos um array de 100 GameObjects, onde todos partilham o mesmo Prototype e a definição do método draw, o que reduz drasticamente a quantidade de memória utilizada.

Quando chamamos o método draw, este vai-se referir à mesma função.

var GameLoop = function() {
    for(gameObject in arrayDeGameObjects) {
        gameObject.draw();
    }
};

Prototype é Um Objecto Vivo

O Prototype de um objecto é em si um objecto vivo, isto quer dizer que, se quisermos alterar a função draw para em vez de desenhar um rectângulo, passe a desenhar um círculo, mesmo após já ter criado todas as instâncias do GameObject,  podemos actualizar o método GameObject.prototype.draw.

Por exemplo:

GameObject.prototype.draw = function() {
    myCanvasContext.arc(this.x, this.y, this.width, 0, Math.PI*2, true);
}

Agora todas as anteriores instâncias do objecto GameObject e futuras instâncias vão desenhar um circlo.

Updating Native Objects Prototypes

Sim é possível. Se for familiar da biblioteca JavaScript Prototype, este é um exemplo de uma Framework que tira partido disto.

Vamos ver um exemplo:

String.prototype.trim = function() {
    return this.replace(/^\s+|\s+$/g, ‘’);
};

Agora podemos aceder a este método em qualquer String:

“ foo bar   “.trim(); // “foo bar”

Existe por sua vez um pequeno problema aqui. Por exemplo, pode utilizar isto na sua aplicação, mas daqui a um ou dois anos, um browser pode implementar uma nova versão do JavaScript que já tenha este método no seu código fonte. O que significa que a sua definição do trim irá alterar a definição nativa. O que não é nada bom, portanto fazer um pequeno check, antes de a criar.

if(!String.prototype.trim) {
    String.prototype.trim = function() {
        return this.replace(/^\s+|\s+$/g, ‘’);
    };
}

Agora se a mesma existir, vai usar a versão nativa do método trim.

Advertisements

4 thoughts on “Prototypes em JavaScript

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s