Definição de Classes ou Objetos ECMAScript

O uso de objetos pré-definidos é apenas uma parte da capacidade de linguagem orientada a objetos, mas sua força real está na capacidade de criar classes e objetos próprios.

ECMAScript possui muitos métodos para criar objetos ou classes.

Método de fábrica

Método original

Porque as propriedades do objeto podem ser definidas dinamicamente após a criação do objeto, muitos desenvolvedores escreveram código semelhante ao seguinte quando o JavaScript foi introduzido inicialmente:

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
};

TIY

No código acima, cria-se o objeto car. Em seguida, configura-se várias propriedades: sua cor é azul, tem quatro portas e pode percorrer 25 milhas por galão. O último atributo é um ponteiro para uma função, o que significa que é um método. Após a execução desse código, é possível usar o objeto car.

No entanto, há um problema, que é a necessidade de criar várias instâncias de car.

Solução: método fábrica

Para resolver esse problema, os desenvolvedores criaram funções fábrica que podem criar e retornar objetos de tipos específicos.

Por exemplo, a função createCar() pode ser usada para encapsular as operações de criação de objetos car listadas anteriormente:

function createCar() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar();
var oCar2 = createCar();

TIY

Aqui, todo o código do primeiro exemplo está contido dentro da função createCar(). Além disso, há uma linha de código extra que retorna o objeto car (oTempCar) como valor da função. Chamar essa função cria um novo objeto e atribui a ele todas as propriedades necessárias, copiando um objeto car que já foi explicado anteriormente. Assim, através desse método, podemos facilmente criar duas versões do objeto car (oCar1 e oCar2), cujas propriedades são completamente idênticas.

Passar parâmetros para a função

Também podemos modificar a função createCar(), passando valores padrão para todas as propriedades, em vez de atribuir valores padrão simplesmente às propriedades:

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//saída "red"
oCar2.showColor();		//saída "blue"

TIY

Adicionar parâmetros à função createCar() permite atribuir valores às propriedades color, doors e mpg do objeto car a ser criado. Isso faz com que dois objetos tenham propriedades idênticas, mas valores diferentes.

Definir métodos de objetos fora da função fábrica

Embora o ECMAScript esteja cada vez mais formalizado, os métodos para criar objetos são ignorados e sua normatização ainda é contestada. Parte é por razões semânticas (parece menos formal do que usar o operador new com o construtor), e parte é por razões funcionais. A razão funcional está na necessidade de criar métodos de objetos dessa maneira. No exemplo anterior, a cada chamada da função createCar(), uma nova função showColor() é criada, o que significa que cada objeto tem sua própria versão showColor(). Na verdade, todos os objetos compartilham da mesma função.

Alguns desenvolvedores definem métodos de objeto fora da função de fábrica, então atribuem o método por meio de uma propriedade para evitar esse problema:

function showColor() {
  alert(this.color);
}
function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = showColor;
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//saída "red"
oCar2.showColor();		//saída "blue"

TIY

Neste código revisado, a função showColor() foi definida antes da função createCar(). Dentro de createCar(), é atribuído um ponteiro para a função showColor() existente ao objeto. Funcionalmente, isso resolve o problema de criar repetidamente objetos de função; mas semanticamente, a função não parece muito como um método do objeto.

Todos esses problemas geraramdefinido pelo desenvolvedoraparição do construtor.

Método de construtor

Criar um construtor é tão fácil quanto criar uma função de fábrica. O primeiro passo é escolher o nome da classe, ou seja, o nome do construtor. Segundo a convenção, a primeira letra é maiúscula para diferenciá-la dos nomes de variáveis que geralmente são minúsculas. Exceto por isso, o construtor parece muito parecido com uma função de fábrica. Considere o seguinte exemplo:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() {
    alert(this.color);
  };
}
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

TIY

Aqui está explicando a diferença entre o código e o método de fábrica. Primeiro, dentro do construtor, não é criado um objeto, mas sim o uso da palavra-chave this. Quando se usa o operador new para o construtor, é criado um objeto antes de executar a primeira linha de código, e apenas com this é possível acessar esse objeto. Em seguida,可以直接 atribuir propriedades a this, pelo padrão, é o valor de retorno do construtor (não é necessário usar explicitamente o operador return).

Agora, ao criar objetos usando o operador new e o nome da classe Car, é mais semelhante à criação de objetos de objetos gerais no ECMAScript.

Você pode perguntar se esse método tem o mesmo problema de gestão de funções que o método anterior? Sim.

Como uma função de fábrica, a função construtora cria repetidamente funções, criando uma versão de função independente para cada objeto. No entanto, como a função de fábrica, também é possível reescrever a função construtora usando uma função externa, o que não tem qualquer significado semântico. Isso é exatamente a vantagem do método de protótipo.

Método de protótipo

Este método usa a propriedade prototype do objeto, que pode ser vista como o原型 que depende da criação de novos objetos.

Aqui, primeiramente usa-se uma função construtora vazia para definir o nome da classe. Em seguida, todas as propriedades e métodos são atribuídos diretamente à propriedade prototype. Reescrevemos o exemplo anterior, o código é o seguinte:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();

TIY

Neste código, primeiramente define-se a função construtora (Car), sem qualquer código. Os próximos vários linhas de código adicionam atributos à propriedade prototype de Car para definir os atributos do objeto Car. Quando chamado new Car(), todas as propriedades do protótipo são imediatamente atribuídas ao objeto a ser criado, o que significa que todos os objetos Car armazenam ponteiros para a função showColor(). Em termos semânticos, todas as propriedades parecem pertencer a um objeto, portanto, resolve os problemas dos dois métodos anteriores.

Além disso, usando essa maneira, você também pode usar o operador instanceof para verificar o tipo do objeto apontado por uma variável dada. Portanto, o seguinte código será output TRUE:

alert(oCar1 instanceof Car);	//Saída "true"

Problemas do método de protótipo

O método de protótipo parece ser uma boa solução. Infelizmente, não atende às expectativas.

Primeiro, essa função construtora não possui parâmetros. Usando o método de protótipo, não é possível inicializar valores de atributos passando parâmetros para a função construtora, porque os atributos color do Car1 e Car2 são todos "blue", os atributos doors são todos 4, e os atributos mpg são todos 25. Isso significa que os valores padrão dos atributos devem ser alterados após a criação do objeto, o que é muito irritante, mas ainda não é tudo. O verdadeiro problema surge quando os atributos apontam para objetos, não para funções. A compartilhamento de funções não causaria problemas, mas os objetos são raramente compartilhados entre várias instâncias. Pense na seguinte exemplo:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();
oCar1.drivers.push("Bill");
alert(oCar1.drivers); // Saída "Mike,John,Bill"
alert(oCar2.drivers); // Saída "Mike,John,Bill"

TIY

No código acima, a propriedade drivers é um ponteiro para um objeto Array, que contém dois nomes "Mike" e "John". Como drivers é um valor de referência, os dois exemplos de Car apontam para o mesmo array. Isso significa que, se adicionar o valor "Bill" ao drivers de oCar1, também será possível vê-lo em oCar2.drivers. Ao exibir qualquer um desses ponteiros, o resultado é a string "Mike,John,Bill".

Devido a tantas questões ao criar objetos, você certamente pensará se há um método razoável de criação de objetos. A resposta é sim, é necessário combinar o construtor e o método de protótipo.

Mistura de construtor/protótipo

A combinação de construtores e métodos de protótipo permite criar objetos de maneira semelhante a outros linguagens de programação. Este conceito é muito simples, ou seja, usar o construtor para definir todas as propriedades não-funcionais do objeto e o método de protótipo para definir as propriedades funcionais (métodos) do objeto. Como resultado, todas as funções são criadas apenas uma vez, e cada objeto possui sua instância de propriedade de objeto.

Reescrevemos o exemplo anterior, o código é o seguinte:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
}
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car("red", 4, 23);
var oCar2 = new Car("blue", 3, 25);
oCar1.drivers.push("Bill");
alert(oCar1.drivers); // Saída "Mike,John,Bill"
alert(oCar2.drivers); // Saída "Mike,John"

TIY

Agora parece mais como criar objetos comuns. Todas as propriedades não-funcionais são criadas no construtor, o que significa que novamente é possível atribuir valores padrão para as propriedades através dos parâmetros do construtor. Como só é criado um exemplo da função showColor(), não há desperdício de memória. Além disso, adicionar o valor "Bill" ao array drivers de oCar1 não afeta o array de oCar2, então ao exibir os valores desses arrays, o oCar1.drivers mostra "Mike,John,Bill", enquanto o oCar2.drivers mostra "Mike,John". Porque foi usado o método de protótipo, ainda é possível usar o operador instanceof para determinar o tipo do objeto.

Este é o método principal adotado pelo ECMAScript, ele possui as características de outros métodos, mas não seus efeitos colaterais. No entanto, alguns desenvolvedores ainda acham que este método não é perfeito.

Método de Protótipo Dinâmico

Para desenvolvedores acostumados a usar outros idiomas, usar a mistura de construtores e protótipos pode não parecer tão harmônico. Afinal, ao definir classes, a maioria das linguagens orientadas a objetos encapsula visualmente atributos e métodos. Considere a classe Java a seguir:

class Car {
  public String color = "blue";
  public int doors = 4;
  public int mpg = 25;
  public Car(String color, int doors, int mpg) {
    this.color = color;
    this.doors = doors;
    this.mpg = mpg;
  }
  public void showColor() {
    System.out.println(color);
  }
}

Java empacota muito bem todas as propriedades e métodos da classe Car, portanto, ao ver este código, sabemos o que ele pretende implementar, definindo informações de um objeto. Críticos do método misto de construtores e protótipos acham que a prática de encontrar atributos dentro do construtor e métodos no exterior não é lógica. Portanto, eles projetaram o método de protótipo dinâmico para proporcionar um estilo de codificação mais amigável.

A ideia básica do método de protótipo dinâmico é semelhante à mistura de construtores e protótipos, ou seja, definir atributos não-funcionais dentro do construtor e atributos funcionais usando atributos de protótipo. A única diferença é a posição de atribuição dos métodos ao objeto. A seguir está a classe Car rewritten usando o método de protótipo dinâmico:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
  if (typeof Car._initialized == "undefined"}) {
    Car.prototype.showColor = function() {
      alert(this.color);
    };
    Car._initialized = true;
  }
}

TIY

Este construtor não muda até que seja verificado se typeof Car._initialized é igual a "undefined" antes. Esta linha de código é a parte mais importante dos métodos dinâmicos de protótipo. Se este valor não for definido, o construtor continuará a definir métodos do objeto de maneira protótipo e depois define Car._initialized como true. Se este valor já estiver definido (seu valor é true, o valor de typeof é Boolean), então não será criado esse método. Em resumo, este método usa um sinalizador (_initialized) para determinar se algum método foi atribuído ao protótipo. Este método é criado e atribuído apenas uma vez, e os desenvolvedores de OOP tradicionais ficarão felizes em ver que o código parece mais uma definição de classe em outros idiomas.

Método fábrica misto

Este método é geralmente usado como uma solução alternativa quando não pode ser aplicado o método anterior. Seu objetivo é criar um falso construtor que apenas retorna uma nova instância de outro objeto.

Este código parece muito semelhante a uma função fábrica:

function Car() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}

TIY

Diferente do modo clássico, este método usa o operador new, tornando-o parecido com um construtor real:

var car = new Car();

Devido ao uso do operador new dentro do construtor Car(), o segundo operador new (localizado fora do construtor) será ignorado, e o objeto criado dentro do construtor será passado de volta para a variável car.

Este método tem o mesmo problema de gerenciamento interno de métodos de objetos que o método clássico. Recomenda-se fortemente: a menos que seja absolutamente necessário, evite usar este método.

Qual maneira usar

Como mencionado anteriormente, o mais amplamente utilizado é a mistura de construtores/funções de protótipo. Além disso, métodos dinâmicos de origem também são populares, equivalentes em função ao modo construtor/função de protótipo. Você pode usar qualquer um desses métodos. No entanto, não use isoladamente o modo construtor ou protótipo clássico, pois isso pode introduzir problemas no código.

Exemplo

Um ponto interessante dos objetos é a maneira como eles resolvem problemas. Um dos problemas mais comuns no ECMAScript é a performance da concatenação de strings. Como em outros idiomas, as strings no ECMAScript são imutáveis, ou seja, seus valores não podem ser alterados. Considere o seguinte código:

var str = "hello ";
str += "world";

Na verdade, os passos executados por trás deste código são os seguintes:

  1. Criar uma string para armazenar "hello ";
  2. Criar uma string para armazenar "world".
  3. Criar uma string para armazenar o resultado da concatenação.
  4. Copiar o conteúdo atual de str para o resultado.
  5. Copiar "world" para o resultado.
  6. Atualizar str para apontar para o resultado.

Cada vez que a concatenação de strings é concluída, são executados os passos 2 a 6, tornando esta operação muito consumidora de recursos. Se este processo for repetido várias centenas, até várias milhares de vezes, pode causar problemas de desempenho. A solução é usar um objeto Array para armazenar as strings e, em seguida, criar a string final usando o método join() (com parâmetro vazio). Imagine substituir o código anterior pelo seguinte:

var arr = new Array();
arr[0] = "hello ";
arr[1] = "world";
var str = arr.join("_");

Dessa forma, não há problema com o número de strings introduzidas no array, pois a concatenação ocorre apenas ao chamar o método join(). Neste momento, os passos executados são os seguintes:

  1. Criar uma string para armazenar o resultado
  2. Copiar cada string para a posição apropriada no resultado

Embora esta solução seja boa, há uma maneira ainda melhor. O problema é que este código não reflete exatamente sua intenção. Para torná-lo mais fácil de entender, você pode empacotar esta funcionalidade na classe StringBuffer:

function StringBuffer () {
  this._strings_ = new Array();
}
StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
};
StringBuffer.prototype.toString = function() {
  return this._strings_.join("");
};

A primeira coisa a notar neste código é a propriedade strings, que tem a intenção de ser uma propriedade privada. Ela possui apenas dois métodos, append() e toString(). O método append() possui um parâmetro, que adiciona o parâmetro ao array de strings, e o método toString() chama o método join() do array, retornando a string realmente concatenada. Para concatenar um grupo de strings usando um objeto StringBuffer, você pode usar o seguinte código:

var buffer = new StringBuffer();
buffer.append("hello ");
buffer.append("world");
var result = buffer.toString();

TIY

Você pode testar o desempenho do objeto StringBuffer e do método de concatenação de strings tradicional usando o seguinte código:

var d1 = new Date();
var str = "";
for (var i=0; i < 10000; i++) {
    str += "text";
}
var d2 = new Date();
document.write("Concatenação com mais (+): ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");
var buffer = new StringBuffer();
d1 = new Date();
for (var i=0; i < 10000; i++) {
    buffer.append("text");
}
var result = buffer.toString();
d2 = new Date();
document.write("<br />Concatenação com StringBuffer: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");

TIY

Este código realiza dois testes de concatenação de strings, o primeiro usando o sinal de mais (+) e o segundo usando a classe StringBuffer. Cada operação concatena 10.000 strings. Os valores de data d1 e d2 são usados para determinar o tempo necessário para completar a operação. Note que, ao criar um objeto Date sem parâmetros, é atribuída a data e hora atuais ao objeto. Para calcular o tempo decorrido na operação de concatenação, subtraia os valores milissegundos da data (usando o retorno do método getTime()). Este é um método comum para medir o desempenho do JavaScript. Os resultados deste teste podem ajudar a comparar a eficiência da classe StringBuffer com a do sinal de mais (+).