Bug de ordenação de hashs no Chrome
Aloha,
A algumas semanas venho trabalhando bastante com JavaScript (tanto para projetos pessoais como 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 renderiza-los normalmente. Usei o seguinte código:
1 2 3 4 | 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 indices 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:
1 2 | 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 mole 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 #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 indices numéricos, onde ele tenta tenta transformar o indice num 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 idéia.
- No caso do Chrome, todos enxergam isso como um “bug” pois a maioria dos outros browser modernos tem ido na direção de ditar o que seria o modo standard de iterar num 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 nem existe posicionamento do pessoal do Chrome para corrigi-lo. Talvez numa revisão do ECMA ou versão nova.
Recomendo a quem quiser ler e tirar sua própria opinião, ler esses dois tópicos (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 para inteiro e assim manterá a ordenação no melhor estilo gambi design patterns.
- Trabalhe com arrays de objetos ao invés de hashs com indices 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, senta e chora.
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 indices 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 de certa forma colocada como standard pelos browsers.
Assim como o W3C vem dando cabeçada atrás de cabeçada para liberar de uma vez novas especificações da HTML e CSS, acho importantíssimo e crítico que o próprio mercado possa “dar a real” e consiga colocar o que interessa para os desenvolvedores como base de aprovação para o novo ECMA.
Simbora.