Uma coisa que sempre intriga muita gente é entender como o Javascript trabalha com objetos. Neste artigo iremos entender como o JavaScript trabalha com objetos, principalmente em dois pontos principais: objetos e vetores. Entenderemos como cada um funciona e atua no contexto programático, de acordo com alguns exemplos que serão mostrados ao longo do artigo.

Primeiro, vamos falar sobre as propriedades. Um monte de valores JavaScript tem outros valores que lhes estão associados. Estas associações são chamadas de propriedades. Cada string tem uma propriedade chamada "lenght", que se refere a um número, a quantidade de caracteres daquela string.

As propriedades podem ser acessadas de duas formas, conforme a Listagem 1.

var texto = "javascript devmedia";
  alert(texto["length"]);
  alert(texto.length);
Listagem 1. Formas de acesso às propriedades em Javascript

O exemplo acima deverá exibir o valor "19".

A segunda maneira é um atalho para a primeira e ela só funciona quando o nome da propriedade é um nome de variável válido, ou seja, quando ele não tem nenhum espaço ou símbolos e não começa com um caractere de dígito.

Os valores nulos e indefinidos não têm quaisquer propriedades. Tentar ler as propriedades de tal valor produzirá um erro. Tente o código da Listagem 2 só para ter uma ideia sobre o tipo de mensagem de erro que seu navegador produz em tal caso (que, para alguns navegadores, pode ser um pouco enigmática).

Listagem 2. Tentando ler o tamanho de um null

  var vazio = null;
  alert(vazio.length); 

Observação: Para que o leitor consiga reproduzir os exemplos deste artigo é necessário criar uma página HTML com uma tag habilitada para JavaScript. A Listagem 3 mostra como fazer isso. Não será foco deste artigo explicar conceitos HTML, pois subentende-se que o leitor já tenha tal conhecimento.

 
  <html>
   <head>
       <script type="text/javascript">
       function teste() {
            var vazio = null;
            alert(vazio.length);
       }
       </script>
   </head>
   <body onload="teste()">
   </body>
  </html>
Listagem 3. Código de execução dos exemplos

O resultado do exemplo não poderá ser visualizado no browser como mensagem de erro HTTP, ou seja, será necessário acessar as ferramentas de desenvolvedor do mesmo. Aqui, usaremos como base o browser Chrome. Para acessar basta selecionar o atalho F12 e acessar a última aba que aparece chamada "Console". É lá onde estarão listados todos os erros relacionados aos scripts client side.

O erro resultante da execução de um valor null será "Uncaught TypeError: Cannot read property 'length' of null" para o Chrome, tal como ilustrado na Figura 1.

Mensagem de erro no Chrome
Figura 1. Mensagem de erro no Chrome

As propriedades de um valor string não podem ser mudadas. Há mais propriedades além do lenght, como veremos, mas não se tem permissão para adicionar ou remover qualquer uma delas.

Isso é diferente com os valores do objeto de texto, pois ele tem suas propriedades e é possível modificá-los, removê-los ou adicionar novos.

Um objeto pode ser escrito assim tal como demonstrado na Listagem 4.

Listagem 4. Representação de objeto em JavaScript

  var sapatos = {cor: "marrom", marca: "Nike", tamanho: 42};
  sapatos.tamanho = 47;
  // Considere que a função show realmente exista e funcione...
  show(sapatos.tamanho);
  delete sapatos.tamanho;
  show(sapatos.tamanho);
  show(sapatos);

Assim como variáveis, cada propriedade anexada a um objeto é marcada por uma string. A primeira instrução cria um objeto em que a propriedade "cor" detém a string "marrom", a propriedade "marca" está ligada à string "Nike", e a propriedade "tamanho" refere-se ao número 42. A segunda declaração dá ao tamanho da propriedade nomeada um novo valor, que é feito da mesma maneira como a modificação de uma variável.

A palavra-chave "delete" remove propriedades específicas. Tentar ler uma propriedade inexistente e terá como valor "undefined".

Se uma propriedade que ainda não existe é definida com o operador de atribuição =, ela é adicionada ao objeto, conforme a Listagem 5.

  var vazio = {};
  vazio.soQueNao = 1000;
  show(vazio.soQueNao); 
Listagem 5. Criando uma propriedade

Propriedades cujos nomes não são nomes de variáveis válidos têm de ser citadas ao criar o objeto, e se marcadas usando colchetes, conforme a Listagem 6.

  var artigo = {"titulo": "Introdução a Javascript", "5": 22};
  show(artigo["5"]);
  artigo["5"] = 20;
  show(artigo[2 + 3]);
  delete artigo["titulo"]; 
Listagem 6. Nome de variável

Como podemos ver, a parte entre parênteses pode ser qualquer expressão. Ela é convertida em uma string para determinar o nome da propriedade a que se refere. Pode-se até usar variáveis para propriedades de nome, conforme a Listagem 7.

  var nomePropriedade = "length";
  var text = "alguma coisa";
  show(text[nomePropriedade]);
Listagem 7. Variáveis para propriedades de nome

E dessa forma temos uma flexibilidade maior no uso e manipulação das variáveis em conjunto com os operadores.

Valores de objetos

Valores de objetos, aparentemente, podem mudar. Os tipos de valores para variáveis em JavaScript são todos imutáveis, o que significa que é impossível alterar um valor existente desses tipos. Você pode combiná-los e obter novos valores a partir deles, mas quando você toma um valor de string específico, o texto dentro dele não pode mudar. Com objetos, por outro lado, o conteúdo de um valor pode ser modificado alterando as suas propriedades.

Quando temos dois números, 2.4 e 2.4, eles podem, para todos os efeitos práticos, serem considerados o mesmo número preciso. Com objetos, existe uma diferença entre ter duas referências para o mesmo objeto e ter dois objetos diferentes que contêm as mesmas propriedades. Considere o código contido na Listagem 8.

  var objeto1 = {valor: 2.4};
  var objeto2 = objeto1;
  var objeto3 = {valor: 2.4};
   
  show(objeto1 == objeto2);
  show(objeto1 == objeto3);
   
  objeto1.valor = 4.8;
  show(objeto2.valor);
  show(objeto3.valor);
 

Os objeto1 e objeto2 são duas variáveis que tem o mesmo valor. Há apenas um objeto de fato, o que explica porque mudando o valor de objeto1 também se altera o valor de objeto2. A variável objeto3 aponta para outro objeto, que inicialmente contém as mesmas propriedades que objeto1, mas vive uma vida separada.

O operador == do JavaScript, ao comparar objetos, só vai retorna "true" se ambos os valores que lhe são atribuídos forem precisamente os mesmos. Ao comparar diferentes objetos com conteúdos idênticos sempre retornará "false". Isso é útil em algumas situações, mas impraticável em outras.

Valores de objetos podem servir para várias coisas diferentes. Comportar-se como um conjunto é só uma dessas coisas.

Vetores em JavaScript

Imagine uma variável no JavaScript que irá armazenar os valores de emails a serem enviados por um sistema de uma empresa. A maneira pela qual o arquivo de email está armazenado ainda é uma pergunta interessante. Ele contém uma série de e-mails e um e-mail pode ser uma string, obviamente. O arquivo inteiro poderia ser colocado em uma string enorme, mas isso é pouco prático. O que queremos é uma coleção de strings distintas.

Coleções de coisas é a finalidade para o qual os objetos são usados. Pode-se fazer um objeto como este:

 var arquivoEmails = {"primeiro e-mail": "Caros, 
     a nossa reunião terá de ser adiada...",
     "segundo e-mail": "..."
     /* e por aí vai ... */}; 

Mas isso faz com que seja difícil de ver os e-mails do início ao fim. Como é que o programa adivinha o nome dessas propriedades? Isso pode ser resolvido por mais nomes de propriedades previsíveis, conforme a Listagem 9.

  var arquivoEmails = {0: "Caros, a nossa reunião terá de ser adiada...
    (email num #1)",
                     1: "(email num #2)",
                     2: "(email num #3)"};
   
  for (var cont = 0; cont in arquivoEmails; cont++)
    print("Processando e-mail #", cont, ": ", 
     arquivoEmails[cont]);
Listagem 9. Propriedade previsíveis

Entretanto, esse tipo de estrutura pode ficar muito confuso, principalmente considerando a quantidade de novos registros possíveis de serem incluídos no modelo. Podemos solucionar a situação utilizando os recursos chamados de vetores (ou arrays). Eles proporcionam várias facilidades, tais como uma propriedade de comprimento, que contém a quantidade de valores no vetor e um número de operações úteis para este tipo de coleção.

Para criar um novo vetor basta usar os colchetes ([ e ]), tal como demonstrado na Listagem 10.

  var arquivoEmails = ["email um", "email dois", "email três"];
  for (var cont = 0; cont < arquivoEmails.length; cont++)
    print("Processando e-mail #", cont, ": ", 
     arquivoEmails[cont]); 
Listagem 10. Criação de novo vetor em JavaScript

Neste exemplo, o número de elementos não é mais especificado explicitamente. O primeiro elemento estará localizado automaticamente na posição 0, o segundo na posição de número 1, e assim por diante.

E essa é uma dúvida comum entre os iniciantes de programação: Por que começar em 0? As pessoas tendem a começar a contar a partir de 1. Tão intuitiva quanto parece, a numeração dos elementos em uma coleção de 0 é muitas vezes mais prático. Começar a partir do elemento 0 também significa que numa coleção com x elementos, o último elemento pode ser encontrado na posição x - 1. É por isso que o loop for na Listagem 10 verifica se "cont < arquivoEmails.length". Não há nenhum elemento na posição arquivoEmails.length, por isso, logo que "cont" recebe esse valor, o loop para também.

Propriedades de objetos e vetores

Tanto uma string quanto um objeto vetor contem, para além da propriedade lenght, um número de propriedades que se referem a valores de função, conforme a Listagem 11.

  var abc = "abc";
  show(typeof abc.toUpperCase);
  show(abc.toUpperCase()); 
Listagem 11. Valores de função

Toda string tem uma propriedade chamada "toUpperCase". Quando chamada, ela irá retornar uma cópia da string em que todas as letras foram convertidas em maiúsculas. Existe também a propriedade contrária, a "toLowerCase", que faz a conversão da string inteira para minúscula.

Observe que, embora a chamada para toUpperCase não passe nenhum argumento, a função, de alguma forma, têm acesso à string "abc", cujo valor é uma propriedade. As propriedades que contêm funções são geralmente chamadas de métodos, como em "toUpperCase é um método de um objeto string". Veja a Listagem 12.

  var lista = [];
  lista.push("arroz");
  lista.push("feijão");
  lista.push("carne");
  show(lista.join(" "));
  show(lista.pop());
  show(lista); 
Listagem 12. Exemplo de vetor e chamadas a funções/métodos

O método "push", o qual está associado com o vetor, pode ser usado para adicionar valores a ele. Logo após acontece uma chamada ao método "pop", o oposto do push que retira e retorna o último valor do vetor. O método "join" constrói uma única grande string a partir de um vetor de strings. O parâmetro passado é entreposto entre os valores no vetor.

O exemplo acima representado atua sobre espaços em brancos para separar diferentes palavras e juntá-las possivelmente em uma frase. As próprias strings são nada mais que objetos que manipulam caracteres para montar palavras, frases e textos. Imagine então como funcionaria a estrutura de um parágrafo nesse mesmo esquema representado.

A primeira pergunta que surge é o que exatamente é um parágrafo. Neste caso, o próprio valor da string não pode nos ajudar muito: o conceito de "texto" para o JavaScript não vai mais fundo do que a ideia de 'sequência de caracteres', por isso, os parágrafos podem ser definidos nesses termos. A forma como ela está dividida e estruturada já constitui outro ponto a ser analisado.

Para que a estrutura seja realmente igual à de um parágrafo, devemos considerar o operador nova linha, que pode ser representado por um caractere no JavaScript e é o que a maioria das pessoas usam para dividir parágrafos. Consideremos um parágrafo como uma parte de um e-mail que começa com um caractere de nova linha ou no início do conteúdo e termina no próximo caractere de nova linha ou no final do conteúdo. A vantagem de usar funções é que eles também já podem existir nativamente, o que significa que alguém as escreveu antes, testou e liberou para todos. As strings já tem um método chamado de "split", que é (quase) o oposto do método "join" dos vetores, pois divide uma string em um vetor, usando a string dada como argumento para determinar em que local deve cortá-la. Veja o exemplo listado na Listagem 13.

  var frase = "Batatinha quando nasce";
  show(frase.split(" ")); 
Listagem 13. Exemplo de quebra de string usando o método split

Neste exemplo podemos dividir uma string em vários pedaços menores, sendo assim, a quebra de novas linhas ("\n"), pode ser usado para dividir um e-mail em parágrafos.

Além disso, pense que talvez você queira que seus parágrafos não comecem com as palavras "Mas" ou "Contudo" e que o nosso programa deva ignorar as mesmas. Como é que vamos testar se uma string começa com uma determinada palavra? O método "charAt" pode ser usado para obter um caráter específico a partir de uma string. "x.charAt(0)" retorna o primeiro caracter, 1 é o segundo, e assim por diante. Uma maneira de verificar se uma string começa com "Mas" seria conforme a Listagem 14.

  var paragrafo = "Mas o único jeito de conseguir vencer é não desistir...";
  show(paragrafo.charAt(0) == "M" && paragrafo.charAt(1) == "a" 
    && paragrafo.charAt(2) == "s"); 
Listagem 14. Verificação da string

Mas isso não está muito bom, pois imagine a verificação de uma palavra de dez caracteres ou trinta. Perceba que quando uma linha fica ridiculamente longa pode ser dividida em várias linhas. O resultado pode ser mais fácil de ler, alinhando o início da nova linha com o primeiro elemento na linha original que desempenha um papel similar. Strings também têm um método chamado "slice", que copia um pedaço da string, a partir do caractere na posição dada pelo primeiro argumento, e terminando antes (não incluindo) do caractere na posição dada pelo segundo. Isso permite que a seleção seja escrita de uma forma mais curta:

show(paragrafo.slice(0, 3) == "Mas); 

Mas o que acontece quando as funções "charAt" ou "slice" são usadas para pegar um pedaço de uma string que não existe? Será que o "startsWith" ainda funciona quando o padrão é maior do que a string é colocada? Veja:

show("Alô".charAt(134));
show("Mar".slice(1, 12));

A função "charAt" vai retornar "" quando não existir um caractere na posição determinada, já "slice" vai simplesmente deixar de fora a parte da nova string que não existe.

Então, sim, essa versão do "startsWith" funciona. Quando a função startsWith("Mercado", "amendoim e canela") é chamada à função slice, porque a string não tem caracteres suficientes, sempre retornará uma string que é menor do que padrão. Por isso, a comparação com == irá retornar falso, o que está correto. Isso ajuda a ter sempre um momento para considerar as entradas anormais (mas válidas) para um programa, o que são geralmente chamadas de "corner cases" e é muito comum que os programas que funcionam perfeitamente em todas as entradas "normais" pararem em um corner case.

Sendo assim poderíamos resumir o código feito anteriormente pela Listagem 15.

var arquivoEmails = recuperarEmails();
  var autores = {"Devmedia": true};
   
  for (var mail = 0; mail < arquivoEmails.length; mail++) {
    var paragrafos = arquivoEmails[mail].split("\n");
    for (var paragrafo = 0;
         paragrafo < paragrafos.length;
         paragrafo++) {
      if (startsWith(paragrafos[paragrafo], "Mas")) {
        var nomes = getNomes(paragrafos[paragrafo]);
        for (var nome = 0; nome < nomes.length; nome++)
          autores[nomes[nome]] = true;
      }
      else if (startsWith(paragrafos[paragrafo], "Contudo")) {
        var nomes = getNomes(paragrafos[paragrafo]);
        for (var nome = 0; nome < nomes.length; nome++)
          delete autores[nomes[nome]];
      }
    }
  }
  show(autores);
Listagem 15. Resumo de funções e métodos chamados

Alguns pedaços de código parecem com uma selva impenetrável. Uma maneira de fazer alguma luz brilhar através dela é só adicionar algumas linhas em branco estratégicas. Isso faz com que pareça melhor, mas não resolve o problema. O que é necessário aqui é quebrar o código acima. Nós já escrevemos duas funções auxiliares, startsWith e getNomes, que cuidam de uma parte pequena, compreensível do problema. Vamos continuar fazendo isso. Veja a Listagem 16.

  function addAoConjunto(set, valores) {
    for (var i = 0; i < valores.length; i++)
      set[valores[i]] = true;
  }
   
  function removeDoConjunto(set, valores) {
    for (var i = 0; i < valores.length; i++)
      delete set[valores[i]];
  }
Listagem 16. Criação de funções auxiliares

Estas duas funções cuidam da adição e remoção de nomes do conjunto. Isso já remove os dois loops mais interiores da solução. Agora veja como fica a nossa função com os novos métodos na Listagem 17.

var autores = {Devmedia: true};
   
  for (var email = 0; email < arquivoEmails.length; email++) {
    var paragrafos = arquivoEmails[email].split("\n");
    for (var paragrafo = 0;
         paragrafo < paragrafos.length;
         paragrafo++) {
      if (startsWith(paragrafos[paragrafo], "Mas"))
        addAoConjunto(autores, getNomes(paragrafos[paragrafo]));
      else if (startsWith(paragrafos[paragrafo], "Contudo"))
        removeDoConjunto(autores, 
          getNomes(paragrafos[paragrafo]));
    }
  }
Listagem 17. Adaptação do método anterior

Por que os métodos addAoConjunto e removeDoConjunto recebem a coleção como um argumento? Eles poderiam usar a variável “autores” diretamente, se quisessem. O motivo é que desta forma eles não são completamente amarrados ao nosso problema atual. A forma como ele está agora é uma ferramenta mais útil em geral.

É útil escrevê-las assim porque elas são "autossuficientes". Elas podem ser lidas e entendidas por conta própria, sem a necessidade de conhecer alguma variável externa chamada autores.

As funções não são puras: elas mudam o objeto passado, como o seu argumento "set". Isso as torna um pouco mais complicadas do que funções puras de verdade, mas ainda assim é tudo muito menos confuso do que funções que mudam qualquer valor ou variável que quiserem.


Saiu na DevMedia!

  • O que são formulários mestre detalhe?: Falhas de gravação é um dos piores cenários que podemos pensar durante o desenvolvimento de software e podemos minimizar esse problema com o padrão Mestre/Detalhe.
  • Construindo uma aplicação mestre detalhe em PHP: Ao longo deste curso veremos como implementar um formulário "mestre detalhe", nos concentrando nos aspectos principais desse relacionamento e como ele é representado no código, na interface do sistema e no banco de dados.
  • Eu sobrevivo sem UML?: Você planeja suas aplicações antes de começar a programar? Ou é daqueles que pensa enquanto escreve? Cuidado, você corre o risco de chegar no meio do projeto sem saber para onde ir. Para evitar isso descubra neste Guia a UML.

Saiba mais sobre PHP ;)

  • JavaScript redirect: Nesta documentação aprenderemos como redirecionar o usuário em JavaScript utilizando window.location.
  • JavaScript switch: Nesta documentação de JavaScript veremos como utilizar a estrutura condicional switch para criar scripts capazes de executar diferentes blocos de código de acordo com diferentes condições.
  • JavaScript if/else: Nesta documentação de JavaScript veremos como utilizar a estrutura condicional if/else para criar scripts com diferentes fluxos de execução.