Desenvolvimento

5 mar, 2012

Bug de ordenação de hashs no Chrome

Publicidade

Há algumas semanas venho trabalhando bastante com JavaScript (tanto para projetos pessoais, como para profissionais) e esbarrei em um problema intrigante, que depois de alguma pesquisa vi que é algo polêmico.

Trabalhando em algumas melhorias para o Sooner, uma extensão para Chrome para trabalhar com o ReadItLater, uma das issues requeridas pelo pessoal era que, ao inserir uma nova página ao serviço, a listagem mantivesse a ordenação, colocando o último item adicionado sempre no topo. O ReadItLater que vou abreviar para RIL trabalha com uma API que trafega dados de duas formas: JSON ou XML.

Obviamente que a escolha foi o JSON, devido à enorme facilidade de manipulação. Depois de adicionar uma nova página e chamar o serviço de “recuperação das páginas”, o RIL me retorna as páginas como no exemplo (extremamente simplificado) abaixo:

{
"list":{
"2":{
"url":"http://url.com",
"time_updated": "20120220180000",
},
"1":{
"url":"http://google.com",
"time_updated": "20120219180000",
}
}
}

Neste exemplo, a coleção list contém um hash com dois objetos, cujos índices são números com uma ordenação definida pelo RIL de acordo com o campo time_update: ele sempre retorna os registros ordenados pela data de atualização em ordem decrescente.

Ok, nada de interessante até aí. Vou fazer uma iteração na lista com um for … in e renderizá-los normalmente. Usei o seguinte código:

for (var pageIndex in ril.list) {
var page = ril.list[pageIndex];
document.write(page.url);
}

Para minha surpresa, o resultado foi:

  • http://google.com;
  • http://url.com.

Ou seja, o Chrome, ao executar a iteração nos objetos, converteu os índices para numéricos e re-ordenou a lista, ignorando a ordem em que eles estavam. Para fazer o teste de São Tomé, abra o Javascript Console do seu Chrome (no Mac é Command + Option + J ou pelo menu View > Developer > JavaScript Console), copie e cole o seguinte código:

var lista = {"2": "2", "1":"1", "a":"a"};
for (var index in lista) { console.log(index) };

O resultado esperado seria esse:

  • 2
  • 1
  • a

Mas o resultado será esse:

  • 1
  • 2
  • a

Agora, abra o console do Safari ou do Firefox, coloque o mesmo código e faça o teste. A iteração dá certo! Seria esse um erro do Chrome, ou eu que estava fazendo algo errado?

O problema polêmico

Depois de pesquisar um pouco, me defrontei com duas issues (#164 do V8 JavaScript Engine que roda dentro do Chrome e o #37404 do projeto Chromium), que discorrem bastante sobre o problema, com pessoas defendendo e argumentando de forma fervorosa vários pontos das RFCs e padrões do ECMA Script 262 (usado como base  das engines JavaScript na maioria dos browsers modernos), fazendo contrapontos com a questão da necessidade de manter uma compatibilidade pelo bem da web como um todo. Basicamente, o que é discutido numa cronologia mais didática é:

  • A (especificação) ECMA-262 não especifica uma ordem de enumeração. O Chrome respeita a ordem de um hash exceto se existirem índices numéricos, onde ele tenta transformar o índice em um integer e ordena as iterações de acordo com esse resultado;
  • De acordo com a especificação do Javascript, “A for…in loop iterates over the properties of an object in an arbitrary order” (https://developer.mozilla.org/en/JavaScript/Reference/Statements/For…in).
  • De acordo com a RFC do JSON (lembrado bem pelo @jeffersongirao), ”An object is an unordered collection of zero or more name/value pairs” (http://www.ietf.org/rfc/rfc4627.txt);
  • Ao mesmo tempo, de acordo com o ECMA-262 (12.6.4) sobre for…in ”The mechanics and order of enumerating the properties (step 6.a in the first  algorithm, step 7.a in the second) is not specified.“ Ou seja, a implementação da enumeração não é especificada e por isso dependente de quem o implementa. O pessoal do Chrome, então, resolveu implementar ao pé da letra (de modo evasivo ou não) enquanto o pessoal dos outros navegadores decidiu fazer de outra forma;
  • Há, de fato, uma interpretação que cada navegador decidiu fazer de um jeito que acha bacana e certo, mas todos concordam que manter a compatibilidade seria uma boa ideia;
  • No caso do Chrome, todos enxergam isso como um “bug”, pois a maioria dos outros browsers modernos têm ido na direção de ditar o que seria o modo standard de iterar em um hash, mantendo sua ordem;
  • Esse “bug” tem trazido muita dor de cabeça para o pessoal, pois força a manter implementações alternativas, ou corrigir comportamentos que devem utilizar essa forma de iterações em hashs ordenados;
  • Até a presente data, o “problema” não foi resolvido e não existe posicionamento do pessoal do Chrome para corrigi-lo. Talvez, em uma revisão do ECMA ou versão nova.

Recomendo a quem quiser ler esses dois tópicos e tirar sua própria opinião (issue #164 e issue #37404): é uma discussão muito bacana e velha (desde 2008). Inclusive o John Resig, criador do jQuery, fala sobre esse e outros bugs num post de 2008.

Importante notar que num recente post do pessoal do Chromium sobre o Harmony, uma versão nova/ revisada do ECMA Script que está sendo feita em conjunto com o comitê do ECMA desde 2008, várias novas features foram apresentadas mas uma em especial ainda está indefinida. Advinha qual é? 

Como resolver

Bom, com um pepino desses para descarcar, existem algumas alternativas para contornar o problema:

  • Quando trabalhar com hashs com indices numéricos, adicione um _ (underline) às chaves para manter a ordenação, como abaixo:
var lista = {"_2": "2", "_1":"1", "a":"a"};
  • Isso evitará que o Chrome faça a conversão por inteiro e assim manterá a ordenação no melhor estilo gambi design patterns;
  • Trabalhe com arrays de objetos ao invés de hashs com índices numéricos. Se você conseguir ter controle na geração do dicionário de dados e precise iterar mantendo a ordem dos itens, transforme seu hash com indices num array de objetos  como abaixo:
    var lista = [{"2": "2"},{"1":"1"},{"a":"a"}];
  •  Se não puder alterar a forma de trabalho de sua array, como por exemplo um resultado que vem de um webservice de terceiros, tente trabalhar de uma forma alternativa como trabalhar os dados em XML.  
  • Se não puder fazer nenhum das alternativas acima, preocupe-se .

Resumo da ópera

Infelizmente, não existe outra forma se não burlar o bug ou refatorar seu programa para que evite trabalhar com hashs com índices numéricos, caso você precise trabalhar com eles numa ordem pré-definida.

Vale a pena acompanhar essa nova proposta do ECMA e torcer para que eles definam de uma vez a forma padrão – e mais ainda, para que seja mantida essa forma, que foi colocada como standard pelos browsers.

Assim como o W3C vem falhando para liberar de uma vez novas especificações da HTML e CSS, acho importantíssimo e crítico que o próprio mercado possa ser sincero e consiga colocar o que interessa para os desenvolvedores como base de aprovação para o novo ECMA.