Boas práticas com JavaScript e jQuery: código não-obstrusivo

Já há bastante tempo, por conta do amadurecimento da Web como plataforma de aplicações, a programação front-end de aplicações para Web vem adotando alguns padrões e boas práticas. Os benefícios disso são claros, principalmente quando consideramos o fato de que até as mais simples páginas Web não são tão triviais: são compostas de camadas distintas de marcação, apresentação e interatividade, normalmente tendo HTML, CSS e JavaScript como responsáveis.

Uma das práticas mais importantes quando pensamos na tríade do front-end é o desacoplamento dessas camadas. Ou seja, não devemos adicionar informações visuais, nem sobre interatividade com o usuário, na camada de marcação. Dessa maneira, a manutenção de cada componente é muito mais fácil pois cada um tem seu lugar distinto. Infelizmente, a mistura ainda ocorre, como este frequente código:

<a style="font-size:16px" href="#" onclick="adicionaItem()">Adicionar item</a>

No exemplo acima, posso distinguir claramente quem não pertence ao HTML, se levarmos em consideração as camadas visuais (style) e de interatividade (onclick). Elas não deveriam estar ali. Para mantermos nossa marcação limpa, podemos utilizar um seletor do CSS para externalizar as informações visuais. Para deixar o exemplo simples, vamos trocar os atributos alienígenas (pertencentes a outras camadas) por um atributo id:

<a id="additem" href="#">Adicionar item</a>

Agora, podemos criar o seguinte seletor no CSS e adicioná-lo ao HTML de maneira adequada (tag <style> ou <link> para arquivo CSS externo):

#additem {
  font-size: 16px;
}

De acordo com o demonstrado no primeiro exemplo, o navegador deve disparar a execução da função JavaScript adicionaItem(), quando o usuário clicar na área da página ocupada pelo elemento <a>. A implementação dessa função não é de nosso interesse no exemplo, só precisamos executá-la. Como fazer isso sem adicionar essa função diretamente na marcação HTML?

Uma das características mais importantes da execução de JavaScript em uma página Web é que o navegador disponibiliza acesso a todo e qualquer elemento declarado no HTML através da DOM API (Document Object Model API). Isso significa que podemos, em nosso código JavaScript, criar objetos que fazem referência direta a tags do HTML.

Alguns desses objetos refletem a alteração de seus atributos imediatamente no navegador. Para implementar o comportamento necessário, devemos informar ao navegador que, em determinado elemento, há uma função a ser executada caso ele seja o target de um evento. Para obtermos esse resutado, precisamos interagir com o EventListener desse elemento. Em JavaScript puro, teríamos a seguinte abordagem:

// Primeiro é necessário criar um objeto que faz
// referência ao elemento no HTML:
var linkAddItem = document.getElementById('additem');

// Depois adicionamos a função "adicionaItem" à lista de
// funções que devem ser executadas quando o usuário clica
// na área do elemento no navegador:
linkAddItem.addEventListener('click', adicionaItem, false);

Esse padrão é o que chamamos de JavaScript não-obstrusivo (não intrusivo). Estamos adicionando interatividade à página através do JavaScript, sem a adição de atributos e informações desnecessárias na marcação. Note que passamos como segundo argumento o nome da função somente, sem os parênteses, necessários para executá-la. Isso porque não queremos executá-la de fato, apenas delegar sua execução à ocorrência do evento.

Fato é que os navegadores (principalmente o IE antigo) não são exatamente consistentes na implementação desses objetos em suas APIs. A própria função addEventListener não existe no IE até sua versão 9. No IE8 e anteriores, devemos chamar a função attachEvent, que implementa o mesmo comportamento.

Para termos um código compatível entre navegadores, seria necessário realizar uma série de verificações para identificar qual abordagem tomar em cada caso. A boa notícia é que algumas bibliotecas de JavaScript fazem isso para nós. Elas abstraem essas diferenças em funções relativamente mais simples e compatíveis com os principais navegadores, versões e plataformas, se não todas.

Eventos com jQuery

O jQuery, hoje a biblioteca JavaScript que é quase onipresente, interage com os EventListeners de maneira bem direta:

$('#additem').on('click', adicionaItem);

Caso você precise usar uma versão anterior a 1.7 do jQuery, substitua a função “on()” pela “bind()”.

No código acima, a função $('#additem') nos retorna um objeto que representa o elemento com id additem em nossa marcação. Esse é um objeto do jQuery e podemos chamar sua função on() que precisa de dois argumentos: o nome do evento a ser observado e o nome da função que deve ser executada ao ocorrer esse evento na área que o elemento ocupa na página. Essa abordagem nos permite descartar o atributo alienígena onclick no HTML, sendo que o próprio JavaScript identifica qual função deve ser executada quando ocorre um evento em determinado elemento da página.

Nos casos em que a função a ser executada precise de argumentos, não podemos passar os argumentos com esse padrão, é necessário passar como argumento para a função on() uma função anônima e, dentro dessa, chamar a função adicionaItem() com argumentos.

Essa função anônima (bem como a função adicionaItem nos exemplos anteriores) recebe, por padrão, um objeto que representa o evento ocorrido no caso de sua execução. Esse objeto contém diversas informações interessantes como um timestamp de quando ocorreu o evento, no caso do evento click, quais eram as coordenadas do mouse na janela do navegador no momento do clique entre outras.

Vamos supor um número como argumento por exemplo:

$('#additem').on('click', function(event) {
    // Essa função anônima pode conter uma lógica mais complexa.
    adicionaItem(1);
});

Por estarmos adicionando um evento a um link nesse exemplo, o comportamento padrão do navegador é, após a execução da função JavaScript, o evento retornar ao seu ciclo normal e o usuário ser levado ao endereço declarado no link, nesse caso o topo da página (href="#"). Normalmente, esse comportamento é indesejado, então vamos informar nossa função que queremos anular o restante do ciclo do evento:

$('#additem').on('click', function(event) {
    // Essa função anônima pode conter uma lógica mais complexa.
    adicionaItem(1);

    // Anular a continuação do ciclo do evento no navegador:
    event.preventDefault();
});

Nos primeiros exemplos, quando passamos o nome da função diretamente à funcão on(), seria necessário adicionar a linha event.preventDefault() dentro da função adicionaItem() para obtermos o comportamento correto. Alguns desenvolvedores também utilizam return false; no lugar de event.preventDefault(); o que, em nosso simples exemplo, terá o mesmo resultado.

Esse tratamento direto do objeto event permite inclusive a adoção de um outro padrão recomendado: nenhum link deve levar ao destino “#” (topo da página), a não ser que seja essa sua finalidade. O ideal é que no atributo href seja utilizado um URL que realizará o mesmo comportamento que esperamos com o JavaScript, só que sem JavaScript. Esse fallback é importante para acessibilidade e atende dispositivos que não suportam JavaScript.

Podemos atribuir uma função ao EventListener de qualquer elemento, não só dos links. Nesses casos, não precisamos anular o comportamento padrão do evento com “event.preventDefault()” visto que clicar em uma <img> ou <div> qualquer não tem efeito colateral na interação do usuário com a página.

Esses e outros assuntos avançados de JavaScript, a gente discute em mais detalhes na Formação Web da Caelum.

39 Comentários

  1. David 04/06/2012 at 15:13 #

    A palavra obstrusivo não existe em português… Se você quer uma tradução para non-obstrusive, use não-intrusivo.

  2. Leonardo Lima 04/06/2012 at 15:21 #

    Muito boa essa dica, a galera tem que se ligar que da para fazer várias coisas com javascript e jquery, valeu.

  3. Paulo Silveira 04/06/2012 at 15:21 #

    oi David. assim como deletar, escanear, deletado, escaneado, di(s)ruptado, nao existiam. os neologismos aparecem conforme a língua evolui, e um adjetivação de um verbo é uma prática mais que aceita por qualquer gramático. ObstruTivo, obstruente, obstruído mostram isso. Mas sem dúvida é apenas um detalhe para o post. Talvez obstruTivo seja o mais correto.

  4. Gabriel Oliveira 04/06/2012 at 15:42 #

    David, obrigado pela sugestão, na verdade o termo mais similar ao “obstrusive” no português seria “protuberante”, no caso algo que está lá chamando a atenção fora do lugar adequado.

    Em nossa área é muito comum a adoção de neologismo por cognação realmente para evitar traduções que levem à compreensão indevida do termo. Isso também facilita relação com material no idioma original da maioria do material disponível.

  5. Thiago 04/06/2012 at 16:37 #

    Na verdade nem existe o termo “obstrusivo”, sempre usaram o termo javascript não-obstruTivo(se for verificar o significado da palavra, é o correto).

    Se a questão e “o que é mais usado?”: vários “estrelinhas” da nossa triste comunidade de devs brasileira também usam o termo obstrutivo, olha:
    http://akitaonrails.com/2010/05/10/rails-3-introducao-a-javascript-nao-obstrusivo-e-responders

    Acho que deviam mudar o título.

  6. Rodrigo 04/06/2012 at 16:42 #

    Post muito bom, principalmente para ajudar entender na prática código não-obstrusivo. Parabéns e obrigado pela contribuição.

  7. Paulo Silveira 04/06/2012 at 16:42 #

    Exato, me parece obstrutivo melhor, como comentei anteriormente.

    Thiago, o link do Akita que voce mandou caiu no mesmo caso. Repare que no permlink ele usou obstruSivo e depois passou para obstruTivo :).

    E também acho que demonstra que a comunidade não é triste, pelo contrário, produzir bastante em português é um sinal positivo. Triste é quando a gente diminui o valor de quem é mais conhecido. Vira ad hominem. Parece que ser conhecido e famoso é negativo. Uma inversão de valores *bem* estranha.

  8. Roberto Souza 04/06/2012 at 16:44 #

    Parabéns! Boa introdução para quem ainda faz aquele macarrão em JavaScript (tem muita gente, inclusive eu, kkkk).

    E pensar que tem gente que vem aqui só para comentar como qual melhor forma de traduzir tal termo, kkkk.

  9. Paulo Silveira 04/06/2012 at 16:57 #

    Coloquei alguns links para traducoes que poderiam ser. Fui ver e Gabriel parece ter razao: o codigo nao esta obstruindo nada. Não impede, apenas é importuno.

  10. Fábio Miranda 04/06/2012 at 18:40 #

    Mto bom post. Valeria a pena também mencionar os Namespaced Events: http://docs.jquery.com/Namespaced_Events

    Melhora a organização dos event handlers e permite usar unbind/off de forma seletiva quando necessário remover algum event handler.

  11. Gabriel Oliveira 04/06/2012 at 18:47 #

    Obrigado Fábio, planejamos deixar o “off” (unbind), assim como delegate, para próximos posts de uma série sobre boas práticas de JavaScript e jQuery.

  12. Caio Ribeiro Pereira 04/06/2012 at 21:38 #

    Muito show esse post! Parabéns!

  13. Tiago Ribeiro 04/06/2012 at 21:45 #

    Parabens Gabriel pelo ótimo post!

  14. Rafael Mueller 05/06/2012 at 09:17 #

    Legal o post,

    Apenas uma correção sobre o event.preventDefault().

    Na verdade “return false;” não terá o mesmo comportamento que o event.preventDefault().

    return false interrompe o bubbling do evento, diferente do preventDefault().

    Coloquei um exemplo em http://jsfiddle.net/3h87g/ para mostrar a diferença.

    O bubbling de evento é muito bem utilizado quando você precisa colocar o mesmo evento em vários elementos do DOM, é bem mais perfomático quando você pode adicionar um handler para o evento em um elemento pai, ao invés de colocar em todos filhos.

  15. Rafael Mueller 05/06/2012 at 09:19 #

    Apenas complementando…

    return false() terá o mesmo comportamento que
    event.preventDefault();
    event.stopPropagation();

  16. Gabriel Oliveira 05/06/2012 at 12:41 #

    Obrigado Rafael, realmente faltou citar que o uso do “return false” teria o mesmo “efeito prático no caso do nosso exemplo”, em algumas situações mais complexas precisamos avaliar o uso correto de preventDefault e stopPropagation.

  17. Gabriel Oliveira 05/06/2012 at 12:45 #

    Ah, Rafael, e quanto à delegar eventos ao handler do elemento pai, teremos um post em breve sobre esse padrão e quando é recomendado adotá-lo.

  18. Edinei 06/06/2012 at 09:19 #

    Muito bom o post @Gabriel, só queria comentar a respeito de alguns possíveis “trade-off” e se puderem comentar a respeito.

    1 – Se todos listeners da minha página forem adicionados da forma como foi apresentado (programaticamente), tenho q navegar pela dom usando ou não algum framework como JQuery, ou seja essa query no DOM pode trazer um carregamento um pouco mais lento. Correto?

    2 – Esses mesmos listeners deveriam ser adicionados após a página ser renderizada (dom:loaded) pois há um risco de tentar adicionar um listener a um elemento e esse ainda nem ter sido renderizado.

    Ou seja realmente vale o esforço (no quesito JS) esse tipo de prática?? Obrigado e abs…

  19. Gabriel Oliveira 06/06/2012 at 16:07 #

    Oi @Edinei, o único caso que acredito ser perceptível a performance menor dessa abordagem é se você tiver milhares de elementos em sua página e cada um precisar de uma função diferente em seu handler.

    Ou se você itera sobre uma coleção de milhares de elementos e passa uma função anônima ao handler de cada um (num bloco “for” por exemplo), daí o navegador cria milhares de funções idênticas. Por isso preferimos a abordagem de passar uma função existente ao handler.

    A busca no DOM não traz nenhuma penalidade de performance, inclusive é um dos pontos de performance que mais evoluiu nos navegadores ultimamente.

    Quanto a esperar o evento DOMContentLoaded para fazer isso, não é exatamente necessário, simplesmente colocar todos os scripts como últimos elementos dentro do body resolve (além de melhorar a performance da página como um todo).

    Se estiver usando jQuery deve estar familiarizado a colocar todos os códigos dentro de uma função anônima:

    $(function() { /* código */ });

    ou

    $(document).ready(function() { /* código */ });

    Isso garante que seu código vá rodar após o DOMContentLoaded, portanto é seguro usar esse padrão.

    Obrigado, abraço!

  20. Felipe Caparelli 13/06/2012 at 11:00 #

    É sempre bom vermos as melhores práticas, eu mesmo utilizo tanto a maneira “incorreta” quanto a “correta”. Acho vantagem nas duas, mas concordo plenamente que colocar o atributo style direto nas tags fica ruim demais pra manutenção. Só que considero a abordagem de usar o atributo onclick muito mais legível para os iniciantes o que ajuda em alguns casos.

    Parabéns pelo post… o blog da Caelum está mesmo com um conteúdo bem diversificado e rico! Excelente!

  21. Otto 13/06/2012 at 12:50 #

    Gostei do artigo. Só fiquei curioso para saber como organiza o javascript de forma a manter a legibilidade e otimização? Cria um único arquivo .js, coloca na mesma página… ?

  22. Bernardo Pires 13/06/2012 at 13:43 #

    Acredito que a informacao no paragrafo abaixo esteja equivocada:

    “Nos casos em que a função a ser executada precise de argumentos, não podemos passar os argumentos com esse padrão, é necessário passar como argumento para a função on() uma função anônima e, dentro dessa, chamar a função adicionaItem() com argumentos.”

    De fato o jquery aceita como 2o parametro da funcao on() um handler qualquer, ou seja, qualquer funcao. Essa funcao sera chamada quando o evento for ativado. O fato de ser anonimo nao faz diferenca nenhuma.

  23. Gabriel Oliveira 13/06/2012 at 15:08 #

    Oi Bernardo, o que quis dizer é o seguinte:

    Caso 1- A função adicionaItem não requer argumentos:

    $(‘#additem’).on(‘click’, adicionaItem);

    Aqui atribuímos a função adicionaItem() ao handler o elemento.

    Caso 2- A função adicionaItem requer um argumento:

    $(‘#additem’).on(‘click’, adicionaItem(‘argValue’));

    Aqui estamos atribuindo o RETORNO da função adicionaItem ao handler, se a função não retornar nada explicitamente, “undefined” será atribuído. Não é isso que queremos. Queremos isso:

    $(‘#additem’).on(‘click’, function(event) { adicionaItem(‘argValue’); });

    Estamos atribuindo uma função ao handler, e essa função executa adicionaItem com o atributo requerido.

  24. Rafael 13/06/2012 at 16:43 #

    Galera muito bom parabéns!

  25. Moacir 14/06/2012 at 16:09 #

    Parabéns pelo post! Está excelente! Muito útil e esclarecedor!

  26. Iderlando 16/06/2012 at 13:14 #

    Muito bom o post, parabéns! E só voltando um pouco ao assunto do primeiro comentário, o adjetivo “obstrusivo” existe sim na língua portuguesa. Uma busca por essa palavra pode ser feito no VOLP, o Vocabulário Ortográfico da Língua Portuguesa no site da ABL. Segue o link http://www.academia.org.br/abl/cgi/cgilua.exe/sys/start.htm?sid=23

  27. Julio Cesar Cabral 19/06/2012 at 10:17 #

    Gabriel,

    Fantástico o seu post. Encaminhei para todos aqui onde eu trabalho para lerem o artigo. Parabéns.

  28. Ivan 05/07/2012 at 13:08 #

    Na boa, o post show desse o cara vem se preocupar com a existência ou não de um termo :/, tem gente q já nasce do contra.

  29. Thadeu esteves 21/12/2012 at 19:04 #

    Muito bom o post, porém ja existem melhores praticas pra se evitar o jQuery , por exemplo , document.querySelector(‘.class’) que faz a mesma coisa que o $…

  30. Gabriel Oliveira 21/12/2012 at 21:01 #

    Oi Thadeu, o maior problema em evitar o jQuery nesse caso não é bem a função que utilizamos para “pegar” um elemento no DOM, e sim o “addEventListener” que está presente no IE somente nas versões 9 e 10. Como o IE 8 ainda é bastante utilizado e ainda precisa ser suportado em muitos projeots, seria preciso fazer sempre uma verificação para sabermos se é necessário usar a função “attachEvent” ao invés da padrão “addEventListener”.

    Vale ressaltar também que a função “$” do jQuery faz muito mais do que simplesmente “pegar” o elemento no DOM…

  31. Andreas 11/04/2013 at 16:07 #

    Parabéns pela iniciativa. Muito boa a explicação.

  32. Leandro 03/05/2013 at 15:50 #

    Parabéns!

  33. Criação 01/10/2013 at 17:10 #

    Olha, cheguei aqui pelo Google e fiquei surpreso com a clareza. Parabens.

  34. Felipe 17/10/2013 at 11:58 #

    Primeiramente Parabéns pelo Post! Muito bom mesmo.

    Encontrei alguns exemplos de contextos para traduções de frases do inglês para o português da palavra utilizada no inglês obstrusive.

    http://www.linguee.com.br/ingles-portugues/traducao/obtrusive.html

  35. Gabriel Oliveira 17/10/2013 at 19:38 #

    Felipe, seu comentário me iluminou… percebi que eu grafei incorretamente a palavra: “obtrusivo” seria o mais correto, embora não conte como verbete em nenhum dicionário.

    Essa é uma discussão para outras gerações, já que eu não ache o neologismo um problema (visto que tem gente que nem as regras básicas do nosso idioma sabe).

    Se você for procurar a tradução de “obtrusive”, verá que encontra “indiscreto, importuno, intruso, intrometido”, e é exatamente isso que eu queria explorar com o post.

    Obrigado!

  36. Fabio Santos 16/12/2014 at 09:25 #

    Excelente post….

Deixe uma resposta